]> git.basschouten.com Git - openhab-addons.git/commitdiff
[tado] Fix AC control bugs; Add null annotations and checks (#12570)
authorAndrew Fiddian-Green <software@whitebear.ch>
Tue, 24 May 2022 20:09:03 +0000 (21:09 +0100)
committerGitHub <noreply@github.com>
Tue, 24 May 2022 20:09:03 +0000 (22:09 +0200)
* [tado] add null annotations and checks
* [tado] cosmetics
* [tado] tweak homeid
* [tado] revert int to Integer
* [tado] explicit method public declaration
* [tado] remove "Bridge not initialized" exception on shutdown
* [tado] new state values: error if unsupported; don't force defaults
* [tado] adopt reviewer suggestion
* [tado] tweaks
* [tado] use new,current,default mode for target capabilities; cosmetics
* [tado] eliminate dead code
* [tado] set or defaults
* [tado] change 'nullable Integer' to 'int'
* [tado] eliminate warning if handler already disposed
* [tado] improve json logging
* [tado] fix getTargetTemperature if state is OFF
* [tado] add null checks
* [tado] log warning instead of throwing exception
* [tado] improved json trace logging
* [tado] adopt reviewer suggestion
* [tado] fix logging to trace
* [tado] thread synch, and fix 'this' assignments on futures
* [tado] tweak yaml
* [tado] fix verticalSwing
* [tado] fix fanLevel

Signed-off-by: Andrew Fiddian-Green <software@whitebear.ch>
21 files changed:
bundles/org.openhab.binding.tado/src/main/api/tado-api.yaml
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/TadoHvacChange.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/adapter/TadoZoneStateAdapter.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/api/HomeApiFactory.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/api/TadoApiTypeUtils.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/builder/AirConditioningZoneSettingsBuilder.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/builder/HeatingZoneSettingsBuilder.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/builder/HotWaterZoneSettingsBuilder.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/builder/TerminationConditionBuilder.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/builder/ZoneSettingsBuilder.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/builder/ZoneStateProvider.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoHomeConfig.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoMobileDeviceConfig.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/config/TadoZoneConfig.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/discovery/TadoDiscoveryService.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/BaseHomeThingHandler.java
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/TadoHandlerFactory.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/TadoMobileDeviceHandler.java
bundles/org.openhab.binding.tado/src/main/java/org/openhab/binding/tado/internal/handler/TadoZoneHandler.java

index 05226ae6ce71522c911412ffc06773c96194acb0..d45f4519fc5ec197e7f0ae24470964f5d3e1c866 100644 (file)
@@ -1050,16 +1050,16 @@ definitions:
         type: array
         items:
           $ref: "#/definitions/ACFanLevel"
-      horizontalSwing:
-        description: Cooling system horizontal swing modes. (Tado confusingly names this array without an 's')
-        type: array
-        items:
-          $ref: "#/definitions/ACHorizontalSwing"
       verticalSwing:
         description: Cooling system vertical swing modes. (Tado confusingly names this array without an 's')
         type: array
         items:
           $ref: "#/definitions/ACVerticalSwing"
+      horizontalSwing:
+        description: Cooling system horizontal swing modes. (Tado confusingly names this array without an 's')
+        type: array
+        items:
+          $ref: "#/definitions/ACHorizontalSwing"
 
   HeatingCapabilities:
     x-discriminator-value: HEATING
index de47df19fcb0b8ccfe7a9bb68b7b8eaddb131b0c..42b1a08c03fa61f0d4df8d842e09fd8870a08cbe 100644 (file)
@@ -14,6 +14,7 @@ package org.openhab.binding.tado.internal;
 
 import java.io.IOException;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanSpeed;
 import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing;
@@ -30,27 +31,30 @@ import org.openhab.binding.tado.internal.builder.ZoneSettingsBuilder;
 import org.openhab.binding.tado.internal.builder.ZoneStateProvider;
 import org.openhab.binding.tado.internal.handler.TadoZoneHandler;
 import org.openhab.core.thing.Thing;
+import org.openhab.core.thing.binding.ThingHandler;
 
 /**
  * Builder for incremental creation of zone overlays.
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TadoHvacChange {
-    private TadoZoneHandler zoneHandler;
+
+    private final TadoZoneHandler zoneHandler;
+    private final TerminationConditionBuilder terminationConditionBuilder;
+    private final ZoneSettingsBuilder settingsBuilder;
 
     private boolean followSchedule = false;
-    private TerminationConditionBuilder terminationConditionBuilder;
-    private ZoneSettingsBuilder settingsBuilder;
 
     public TadoHvacChange(Thing zoneThing) {
-        if (!(zoneThing.getHandler() instanceof TadoZoneHandler)) {
+        ThingHandler handler = zoneThing.getHandler();
+        if (!(handler instanceof TadoZoneHandler)) {
             throw new IllegalArgumentException("TadoZoneThing expected, but instead got " + zoneThing);
         }
-
-        this.zoneHandler = (TadoZoneHandler) zoneThing.getHandler();
-        this.terminationConditionBuilder = TerminationConditionBuilder.of(zoneHandler);
-        this.settingsBuilder = ZoneSettingsBuilder.of(zoneHandler);
+        zoneHandler = (TadoZoneHandler) handler;
+        terminationConditionBuilder = TerminationConditionBuilder.of(zoneHandler);
+        settingsBuilder = ZoneSettingsBuilder.of(zoneHandler);
     }
 
     public TadoHvacChange withOperationMode(OperationMode operationMode) {
@@ -60,12 +64,11 @@ public class TadoHvacChange {
             case MANUAL:
                 return activeForever();
             case TIMER:
-                return activeFor(null);
+                return activeForMinutes(0);
             case UNTIL_CHANGE:
                 return activeUntilChange();
-            default:
-                return this;
         }
+        return this;
     }
 
     public TadoHvacChange followSchedule() {
@@ -83,9 +86,9 @@ public class TadoHvacChange {
         return this;
     }
 
-    public TadoHvacChange activeFor(Integer minutes) {
+    public TadoHvacChange activeForMinutes(int minutes) {
         terminationConditionBuilder.withTerminationType(OverlayTerminationConditionType.TIMER);
-        terminationConditionBuilder.withTimerDurationInSeconds(minutes != null ? minutes * 60 : null);
+        terminationConditionBuilder.withTimerDurationInSeconds(minutes * 60);
         return this;
     }
 
index 4e80dc9f08a2655ff820957e4d9ddafd4ce87515..ed33bac2007e762b9416cf9b6153fd35a4fdaa5f 100644 (file)
@@ -16,6 +16,7 @@ import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.OffsetDateTime;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.tado.internal.TadoBindingConstants.HvacMode;
 import org.openhab.binding.tado.internal.TadoBindingConstants.OperationMode;
 import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
@@ -31,6 +32,7 @@ import org.openhab.binding.tado.internal.api.model.HeatingZoneSetting;
 import org.openhab.binding.tado.internal.api.model.HotWaterZoneSetting;
 import org.openhab.binding.tado.internal.api.model.Overlay;
 import org.openhab.binding.tado.internal.api.model.OverlayTerminationConditionType;
+import org.openhab.binding.tado.internal.api.model.PercentageDataPoint;
 import org.openhab.binding.tado.internal.api.model.Power;
 import org.openhab.binding.tado.internal.api.model.SensorDataPoints;
 import org.openhab.binding.tado.internal.api.model.TadoSystemType;
@@ -55,9 +57,10 @@ import org.openhab.core.types.UnDefType;
  * @author Andrew Fiddian-Green - Added Low Battery Alarm, A/C Power and Open Window channels
  *
  */
+@NonNullByDefault
 public class TadoZoneStateAdapter {
-    private ZoneState zoneState;
-    private TemperatureUnit temperatureUnit;
+    private final ZoneState zoneState;
+    private final TemperatureUnit temperatureUnit;
 
     public TadoZoneStateAdapter(ZoneState zoneState, TemperatureUnit temperatureUnit) {
         this.zoneState = zoneState;
@@ -69,10 +72,9 @@ public class TadoZoneStateAdapter {
         return toTemperatureState(sensorDataPoints.getInsideTemperature(), temperatureUnit);
     }
 
-    public DecimalType getHumidity() {
-        SensorDataPoints sensorDataPoints = zoneState.getSensorDataPoints();
-        return sensorDataPoints.getHumidity() != null ? toDecimalType(sensorDataPoints.getHumidity().getPercentage())
-                : null;
+    public State getHumidity() {
+        PercentageDataPoint humidity = zoneState.getSensorDataPoints().getHumidity();
+        return humidity != null ? toDecimalType(humidity.getPercentage()) : UnDefType.UNDEF;
     }
 
     public DecimalType getHeatingPower() {
@@ -81,7 +83,7 @@ public class TadoZoneStateAdapter {
                 : DecimalType.ZERO;
     }
 
-    public OnOffType getAcPower() {
+    public State getAcPower() {
         ActivityDataPoints dataPoints = zoneState.getActivityDataPoints();
         AcPowerDataPoint acPower = dataPoints.getAcPower();
         if (acPower != null) {
@@ -90,7 +92,7 @@ public class TadoZoneStateAdapter {
                 return OnOffType.from(acPowerValue);
             }
         }
-        return null;
+        return UnDefType.UNDEF;
     }
 
     public StringType getMode() {
@@ -106,6 +108,9 @@ public class TadoZoneStateAdapter {
     }
 
     public State getTargetTemperature() {
+        if (!isPowerOn()) {
+            return UnDefType.UNDEF;
+        }
         switch (zoneState.getSetting().getType()) {
             case HEATING:
                 return toTemperatureState(((HeatingZoneSetting) zoneState.getSetting()).getTemperature(),
@@ -228,20 +233,12 @@ public class TadoZoneStateAdapter {
     }
 
     private static State toTemperatureState(TemperatureObject temperature, TemperatureUnit temperatureUnit) {
-        if (temperature == null) {
-            return UnDefType.NULL;
-        }
-
         return temperatureUnit == TemperatureUnit.FAHRENHEIT
                 ? new QuantityType<>(temperature.getFahrenheit(), ImperialUnits.FAHRENHEIT)
                 : new QuantityType<>(temperature.getCelsius(), SIUnits.CELSIUS);
     }
 
     private static State toTemperatureState(TemperatureDataPoint temperature, TemperatureUnit temperatureUnit) {
-        if (temperature == null) {
-            return UnDefType.NULL;
-        }
-
         return temperatureUnit == TemperatureUnit.FAHRENHEIT
                 ? new QuantityType<>(temperature.getFahrenheit(), ImperialUnits.FAHRENHEIT)
                 : new QuantityType<>(temperature.getCelsius(), SIUnits.CELSIUS);
index 75a38dbfb53a698530c7be656cb58ea1e1b6d661..adc8dc4cfc1a1204bddd0601c85357a1f5d7b7ff 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.tado.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.tado.internal.api.auth.Authorizer;
 import org.openhab.binding.tado.internal.api.auth.OAuthAuthorizer;
 import org.openhab.binding.tado.internal.api.client.HomeApi;
@@ -23,6 +24,7 @@ import com.google.gson.Gson;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class HomeApiFactory {
     private static final String OAUTH_SCOPE = "home.user";
     private static final String OAUTH_CLIENT_ID = "public-api-preview";
index 909648f9b34b98627f905da9efceb0235d07d681..86e4235c7d858e2c61a1f50dd384e634ba60ec93 100644 (file)
@@ -12,6 +12,8 @@
  */
 package org.openhab.binding.tado.internal.api;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanSpeed;
 import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing;
@@ -25,6 +27,7 @@ import org.openhab.binding.tado.internal.api.model.AcFanSpeed;
 import org.openhab.binding.tado.internal.api.model.AcMode;
 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.GenericZoneCapabilities;
 import org.openhab.binding.tado.internal.api.model.ManualTerminationCondition;
 import org.openhab.binding.tado.internal.api.model.OverlayTerminationCondition;
 import org.openhab.binding.tado.internal.api.model.OverlayTerminationConditionTemplate;
@@ -39,9 +42,11 @@ import org.openhab.binding.tado.internal.api.model.TimerTerminationConditionTemp
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TadoApiTypeUtils {
+
     public static OverlayTerminationCondition getTerminationCondition(OverlayTerminationConditionType type,
-            Integer timerDurationInSeconds) {
+            int timerDurationInSeconds) {
         switch (type) {
             case TIMER:
                 return timerTermination(timerDurationInSeconds);
@@ -50,26 +55,38 @@ public class TadoApiTypeUtils {
             case TADO_MODE:
                 return tadoModeTermination();
             default:
-                return null;
+                throw new IllegalArgumentException("Unexpected OverlayTerminationConditionType " + type);
         }
     }
 
     public static OverlayTerminationCondition cleanTerminationCondition(
             OverlayTerminationCondition terminationCondition) {
-        Integer timerDuration = terminationCondition.getType() == OverlayTerminationConditionType.TIMER
-                ? ((TimerTerminationCondition) terminationCondition).getRemainingTimeInSeconds()
-                : null;
+        OverlayTerminationConditionType conditionType = terminationCondition.getType();
 
-        return getTerminationCondition(terminationCondition.getType(), timerDuration);
+        int timerDuration = 0;
+        if (conditionType == OverlayTerminationConditionType.TIMER) {
+            Integer duration = ((TimerTerminationCondition) terminationCondition).getRemainingTimeInSeconds();
+            if (duration != null) {
+                timerDuration = duration.intValue();
+            }
+        }
+
+        return getTerminationCondition(conditionType, timerDuration);
     }
 
     public static OverlayTerminationCondition terminationConditionTemplateToTerminationCondition(
             OverlayTerminationConditionTemplate template) {
-        Integer timerDuration = template.getType() == OverlayTerminationConditionType.TIMER
-                ? ((TimerTerminationConditionTemplate) template).getDurationInSeconds()
-                : null;
+        OverlayTerminationConditionType conditionType = template.getType();
 
-        return getTerminationCondition(template.getType(), timerDuration);
+        int timerDuration = 0;
+        if (conditionType == OverlayTerminationConditionType.TIMER) {
+            Integer duration = ((TimerTerminationConditionTemplate) template).getDurationInSeconds();
+            if (duration != null) {
+                timerDuration = duration.intValue();
+            }
+        }
+
+        return getTerminationCondition(conditionType, timerDuration);
     }
 
     public static TimerTerminationCondition timerTermination(int durationInSeconds) {
@@ -103,18 +120,10 @@ public class TadoApiTypeUtils {
     }
 
     public static Float getTemperatureInUnit(TemperatureObject temperature, TemperatureUnit temperatureUnit) {
-        if (temperature == null) {
-            return null;
-        }
-
         return temperatureUnit == TemperatureUnit.FAHRENHEIT ? temperature.getFahrenheit() : temperature.getCelsius();
     }
 
     public static AcMode getAcMode(HvacMode mode) {
-        if (mode == null) {
-            return null;
-        }
-
         switch (mode) {
             case HEAT:
                 return AcMode.HEAT;
@@ -127,15 +136,11 @@ public class TadoApiTypeUtils {
             case AUTO:
                 return AcMode.AUTO;
             default:
-                return null;
+                throw new IllegalArgumentException("Unexpected AcMode " + mode);
         }
     }
 
     public static AcFanSpeed getAcFanSpeed(FanSpeed fanSpeed) {
-        if (fanSpeed == null) {
-            return null;
-        }
-
         switch (fanSpeed) {
             case AUTO:
                 return AcFanSpeed.AUTO;
@@ -145,16 +150,12 @@ public class TadoApiTypeUtils {
                 return AcFanSpeed.MIDDLE;
             case LOW:
                 return AcFanSpeed.LOW;
+            default:
+                throw new IllegalArgumentException("Unexpected AcFanSpeed " + fanSpeed);
         }
-
-        return null;
     }
 
     public static ACFanLevel getFanLevel(FanLevel fanLevel) {
-        if (fanLevel == null) {
-            return null;
-        }
-
         switch (fanLevel) {
             case AUTO:
                 return ACFanLevel.AUTO;
@@ -170,16 +171,12 @@ public class TadoApiTypeUtils {
                 return ACFanLevel.LEVEL5;
             case SILENT:
                 return ACFanLevel.SILENT;
+            default:
+                throw new IllegalArgumentException("Unexpected FanLevel " + fanLevel);
         }
-
-        return null;
     }
 
     public static ACHorizontalSwing getHorizontalSwing(HorizontalSwing horizontalSwing) {
-        if (horizontalSwing == null) {
-            return null;
-        }
-
         switch (horizontalSwing) {
             case LEFT:
                 return ACHorizontalSwing.LEFT;
@@ -197,16 +194,12 @@ public class TadoApiTypeUtils {
                 return ACHorizontalSwing.OFF;
             case AUTO:
                 return ACHorizontalSwing.AUTO;
+            default:
+                throw new IllegalArgumentException("Unexpected HorizontalSwing " + horizontalSwing);
         }
-
-        return null;
     }
 
     public static ACVerticalSwing getVerticalSwing(VerticalSwing verticalSwing) {
-        if (verticalSwing == null) {
-            return null;
-        }
-
         switch (verticalSwing) {
             case AUTO:
                 return ACVerticalSwing.AUTO;
@@ -224,34 +217,34 @@ public class TadoApiTypeUtils {
                 return ACVerticalSwing.ON;
             case OFF:
                 return ACVerticalSwing.OFF;
+            default:
+                throw new IllegalArgumentException("Unexpected VerticalSwing " + verticalSwing);
         }
-
-        return null;
     }
 
-    public static AcModeCapabilities getModeCapabilities(AirConditioningCapabilities capabilities, AcMode mode) {
-        AcModeCapabilities modeCapabilities = null;
+    public static AcModeCapabilities getModeCapabilities(AcMode acMode,
+            @Nullable GenericZoneCapabilities capabilities) {
+        AirConditioningCapabilities acCapabilities;
 
-        if (mode != null) {
-            switch (mode) {
-                case COOL:
-                    modeCapabilities = capabilities.getCOOL();
-                    break;
-                case HEAT:
-                    modeCapabilities = capabilities.getHEAT();
-                    break;
-                case DRY:
-                    modeCapabilities = capabilities.getDRY();
-                    break;
-                case AUTO:
-                    modeCapabilities = capabilities.getAUTO();
-                    break;
-                case FAN:
-                    modeCapabilities = capabilities.getFAN();
-                    break;
-            }
+        if (capabilities instanceof AirConditioningCapabilities) {
+            acCapabilities = (AirConditioningCapabilities) capabilities;
+        } else {
+            acCapabilities = new AirConditioningCapabilities();
         }
 
-        return modeCapabilities != null ? modeCapabilities : new AcModeCapabilities();
+        switch (acMode) {
+            case COOL:
+                return acCapabilities.getCOOL();
+            case HEAT:
+                return acCapabilities.getHEAT();
+            case DRY:
+                return acCapabilities.getDRY();
+            case AUTO:
+                return acCapabilities.getAUTO();
+            case FAN:
+                return acCapabilities.getFAN();
+            default:
+                throw new IllegalArgumentException("Unexpected AcMode " + acMode);
+        }
     }
 }
index 0a5a3c605148f38a008782c43ba206ea4efb4bbb..e914c9f56d281738a666d883f8d8e5a469aae743 100644 (file)
@@ -17,16 +17,21 @@ import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.*;
 import java.io.IOException;
 import java.util.List;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
+import org.openhab.binding.tado.internal.TadoBindingConstants.FanSpeed;
+import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing;
 import org.openhab.binding.tado.internal.TadoBindingConstants.HvacMode;
 import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
+import org.openhab.binding.tado.internal.TadoBindingConstants.VerticalSwing;
 import org.openhab.binding.tado.internal.api.ApiException;
+import org.openhab.binding.tado.internal.api.TadoApiTypeUtils;
 import org.openhab.binding.tado.internal.api.model.ACFanLevel;
 import org.openhab.binding.tado.internal.api.model.ACHorizontalSwing;
 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.AcMode;
 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.CoolingZoneSetting;
 import org.openhab.binding.tado.internal.api.model.GenericZoneCapabilities;
 import org.openhab.binding.tado.internal.api.model.GenericZoneSetting;
@@ -35,17 +40,23 @@ 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.TemperatureObject;
 import org.openhab.binding.tado.internal.api.model.TemperatureRange;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  *
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class AirConditioningZoneSettingsBuilder extends ZoneSettingsBuilder {
     private static final AcMode DEFAULT_MODE = AcMode.COOL;
     private static final float DEFAULT_TEMPERATURE_C = 20.0f;
     private static final float DEFAULT_TEMPERATURE_F = 68.0f;
 
+    private static final String STATE_VALUE_NOT_SUPPORTED = "Your a/c unit does not support '{}:{}' when in state '{}:{}', (supported values: [{}]).";
+    private Logger logger = LoggerFactory.getLogger(AirConditioningZoneSettingsBuilder.class);
+
     @Override
     public GenericZoneSetting build(ZoneStateProvider zoneStateProvider, GenericZoneCapabilities genericCapabilities)
             throws IOException, ApiException {
@@ -53,95 +64,138 @@ public class AirConditioningZoneSettingsBuilder extends ZoneSettingsBuilder {
             return coolingSetting(false);
         }
 
-        CoolingZoneSetting setting = coolingSetting(true);
-        setting.setMode(getAcMode(mode));
+        CoolingZoneSetting newSetting = coolingSetting(true);
+
+        AcMode targetMode;
+        HvacMode mode = this.mode;
+        if (mode != null) {
+            targetMode = getAcMode(mode);
+            newSetting.setMode(targetMode);
+        } else {
+            // if mode not changing, so the reference is the current (or default) mode
+            targetMode = getCurrentOrDefaultAcMode(zoneStateProvider);
+        }
+
+        Float temperature = this.temperature;
         if (temperature != null) {
-            setting.setTemperature(temperature(temperature, temperatureUnit));
+            newSetting.setTemperature(temperature(temperature, temperatureUnit));
         }
 
+        Boolean swing = this.swing;
         if (swing != null) {
-            setting.setSwing(swing ? Power.ON : Power.OFF);
+            newSetting.setSwing(swing.booleanValue() ? Power.ON : Power.OFF);
         }
 
+        Boolean light = this.light;
         if (light != null) {
-            setting.setLight(light ? Power.ON : Power.OFF);
+            newSetting.setLight(light.booleanValue() ? Power.ON : Power.OFF);
         }
 
+        FanSpeed fanSpeed = this.fanSpeed;
         if (fanSpeed != null) {
-            setting.setFanSpeed(getAcFanSpeed(fanSpeed));
+            newSetting.setFanSpeed(getAcFanSpeed(fanSpeed));
         }
 
+        /*
+         * In the latest API release Tado introduced extra AC settings that have an open ended list of possible
+         * supported state values. And for any particular device, its specific list of supported values is available
+         * via its 'capabilities' structure. So before setting a new value, we check if the respective new value is in
+         * the capabilities list that corresponds to the target AC mode. And if not, a warning message is logged.
+         */
+        AcModeCapabilities targetModeCapabilities = TadoApiTypeUtils.getModeCapabilities(targetMode,
+                genericCapabilities);
+
+        FanLevel fanLevel = this.fanLevel;
         if (fanLevel != null) {
-            setting.setFanLevel(getFanLevel(fanLevel));
+            ACFanLevel targetFanLevel = getFanLevel(fanLevel);
+            List<ACFanLevel> targetFanLevels = targetModeCapabilities.getFanLevel();
+            if (targetFanLevels != null && targetFanLevels.contains(targetFanLevel)) {
+                newSetting.setFanLevel(targetFanLevel);
+            } else {
+                logger.warn(STATE_VALUE_NOT_SUPPORTED, targetFanLevel.getClass().getSimpleName(), targetFanLevel,
+                        targetMode.getClass().getSimpleName(), targetMode, targetFanLevels);
+            }
         }
 
+        HorizontalSwing horizontalSwing = this.horizontalSwing;
         if (horizontalSwing != null) {
-            setting.setHorizontalSwing(getHorizontalSwing(horizontalSwing));
+            ACHorizontalSwing targetHorizontalSwing = getHorizontalSwing(horizontalSwing);
+            List<ACHorizontalSwing> targetHorizontalSwings = targetModeCapabilities.getHorizontalSwing();
+            if (targetHorizontalSwings != null && targetHorizontalSwings.contains(targetHorizontalSwing)) {
+                newSetting.setHorizontalSwing(targetHorizontalSwing);
+            } else {
+                logger.warn(STATE_VALUE_NOT_SUPPORTED, targetHorizontalSwing.getClass().getSimpleName(),
+                        targetHorizontalSwing, targetMode.getClass().getSimpleName(), targetMode,
+                        targetHorizontalSwings);
+            }
         }
 
+        VerticalSwing verticalSwing = this.verticalSwing;
         if (verticalSwing != null) {
-            setting.setVerticalSwing(getVerticalSwing(verticalSwing));
+            ACVerticalSwing targetVerticalSwing = getVerticalSwing(verticalSwing);
+            List<ACVerticalSwing> targetVerticalSwings = targetModeCapabilities.getVerticalSwing();
+            if (targetVerticalSwings != null && targetVerticalSwings.contains(targetVerticalSwing)) {
+                newSetting.setVerticalSwing(targetVerticalSwing);
+            } else {
+                logger.warn(STATE_VALUE_NOT_SUPPORTED, targetVerticalSwing.getClass().getSimpleName(),
+                        targetVerticalSwing, targetMode.getClass().getSimpleName(), targetMode, targetVerticalSwings);
+            }
         }
 
-        addMissingSettingParts(zoneStateProvider, genericCapabilities, setting);
+        addMissingSettingParts(zoneStateProvider, genericCapabilities, newSetting);
 
-        return setting;
+        return newSetting;
     }
 
     private void addMissingSettingParts(ZoneStateProvider zoneStateProvider,
-            GenericZoneCapabilities genericCapabilities, CoolingZoneSetting setting) throws IOException, ApiException {
-        if (setting.getMode() == null) {
+            GenericZoneCapabilities genericCapabilities, CoolingZoneSetting newSetting)
+            throws IOException, ApiException {
+        if (newSetting.getMode() == null) {
             AcMode targetMode = getCurrentOrDefaultAcMode(zoneStateProvider);
-            setting.setMode(targetMode);
+            newSetting.setMode(targetMode);
         }
 
-        AcModeCapabilities capabilities = getModeCapabilities((AirConditioningCapabilities) genericCapabilities,
-                setting.getMode());
+        AcModeCapabilities targetCapabilities = getModeCapabilities(newSetting.getMode(), genericCapabilities);
 
-        TemperatureRange temperatures = capabilities.getTemperatures();
-        if (temperatures != null && setting.getTemperature() == null) {
-            setting.setTemperature(getCurrentOrDefaultTemperature(zoneStateProvider, temperatures));
+        TemperatureRange temperatures = targetCapabilities.getTemperatures();
+        if (temperatures != null && newSetting.getTemperature() == null) {
+            newSetting.setTemperature(getCurrentOrDefaultTemperature(zoneStateProvider, temperatures));
         }
 
-        List<AcFanSpeed> fanSpeeds = capabilities.getFanSpeeds();
-        if (fanSpeeds != null && !fanSpeeds.isEmpty() && setting.getFanSpeed() == null) {
-            setting.setFanSpeed(getCurrentOrDefaultFanSpeed(zoneStateProvider, fanSpeeds));
+        List<AcFanSpeed> fanSpeeds = targetCapabilities.getFanSpeeds();
+        if (fanSpeeds != null && !fanSpeeds.isEmpty() && newSetting.getFanSpeed() == null) {
+            newSetting.setFanSpeed(getCurrentOrDefaultFanSpeed(zoneStateProvider, fanSpeeds));
         }
 
-        List<Power> swings = capabilities.getSwings();
-        if (swings != null && !swings.isEmpty() && setting.getSwing() == null) {
-            setting.setSwing(getCurrentOrDefaultSwing(zoneStateProvider, swings));
+        List<Power> swings = targetCapabilities.getSwings();
+        if (swings != null && !swings.isEmpty() && newSetting.getSwing() == null) {
+            newSetting.setSwing(getCurrentOrDefaultSwing(zoneStateProvider, swings));
         }
 
-        // Tado confusingly calls the List / getter method 'fanLevel' / 'getFanLevel()' without 's'
-        List<ACFanLevel> fanLevels = capabilities.getFanLevel();
-        if (fanLevels != null && !fanLevels.isEmpty() && setting.getFanLevel() == null) {
-            setting.setFanLevel(getCurrentOrDefaultFanLevel(zoneStateProvider, fanLevels));
+        List<ACFanLevel> fanLevels = targetCapabilities.getFanLevel();
+        if (fanLevels != null && !fanLevels.isEmpty() && newSetting.getFanLevel() == null) {
+            newSetting.setFanLevel(getCurrentOrDefaultFanLevel(zoneStateProvider, fanLevels));
         }
 
-        // Tado confusingly calls the List / getter method 'horizontalSwing' / 'getHorizontalSwing()' without 's'
-        List<ACHorizontalSwing> horizontalSwings = capabilities.getHorizontalSwing();
-        if (horizontalSwings != null && !horizontalSwings.isEmpty() && setting.getHorizontalSwing() == null) {
-            setting.setHorizontalSwing(getCurrentOrDefaultHorizontalSwing(zoneStateProvider, horizontalSwings));
+        List<ACHorizontalSwing> horizontalSwings = targetCapabilities.getHorizontalSwing();
+        if (horizontalSwings != null && !horizontalSwings.isEmpty() && newSetting.getHorizontalSwing() == null) {
+            newSetting.setHorizontalSwing(getCurrentOrDefaultHorizontalSwing(zoneStateProvider, horizontalSwings));
         }
 
-        // Tado confusingly calls the List / getter method 'verticalSwing' / 'getVerticalSwing()' without 's'
-        List<ACVerticalSwing> verticalSwings = capabilities.getVerticalSwing();
-        if (verticalSwings != null && !verticalSwings.isEmpty() && setting.getVerticalSwing() == null) {
-            setting.setVerticalSwing(getCurrentOrDefaultVerticalSwing(zoneStateProvider, verticalSwings));
+        List<ACVerticalSwing> verticalSwings = targetCapabilities.getVerticalSwing();
+        if (verticalSwings != null && !verticalSwings.isEmpty() && newSetting.getVerticalSwing() == null) {
+            newSetting.setVerticalSwing(getCurrentOrDefaultVerticalSwing(zoneStateProvider, verticalSwings));
         }
 
-        // Tado confusingly calls the List / getter method 'light' / 'getLight()' without 's'
-        List<Power> lights = capabilities.getLight();
-        if (lights != null && !lights.isEmpty() && setting.getLight() == null) {
-            setting.setLight(getCurrentOrDefaultLight(zoneStateProvider, lights));
+        List<Power> lights = targetCapabilities.getLight();
+        if (lights != null && !lights.isEmpty() && newSetting.getLight() == null) {
+            newSetting.setLight(getCurrentOrDefaultLight(zoneStateProvider, lights));
         }
     }
 
     private AcMode getCurrentOrDefaultAcMode(ZoneStateProvider zoneStateProvider) throws IOException, ApiException {
-        CoolingZoneSetting zoneSetting = (CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting();
-
-        return zoneSetting.getMode() != null ? zoneSetting.getMode() : DEFAULT_MODE;
+        AcMode acMode = ((CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting()).getMode();
+        return acMode != null ? acMode : DEFAULT_MODE;
     }
 
     private TemperatureObject getCurrentOrDefaultTemperature(ZoneStateProvider zoneStateProvider,
@@ -165,68 +219,40 @@ public class AirConditioningZoneSettingsBuilder extends ZoneSettingsBuilder {
 
     private AcFanSpeed getCurrentOrDefaultFanSpeed(ZoneStateProvider zoneStateProvider, List<AcFanSpeed> fanSpeeds)
             throws IOException, ApiException {
-        CoolingZoneSetting zoneSetting = (CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting();
-
-        if (zoneSetting.getFanSpeed() != null && fanSpeeds.contains(zoneSetting.getFanSpeed())) {
-            return zoneSetting.getFanSpeed();
-        }
-
-        return fanSpeeds.get(0);
+        AcFanSpeed fanSpeed = ((CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting()).getFanSpeed();
+        return (fanSpeed != null) && fanSpeeds.contains(fanSpeed) ? fanSpeed : fanSpeeds.get(0);
     }
 
     private Power getCurrentOrDefaultSwing(ZoneStateProvider zoneStateProvider, List<Power> swings)
             throws IOException, ApiException {
-        CoolingZoneSetting zoneSetting = (CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting();
-
-        if (zoneSetting.getSwing() != null && swings.contains(zoneSetting.getSwing())) {
-            return zoneSetting.getSwing();
-        }
-
-        return swings.get(0);
-    }
-
-    private Power getCurrentOrDefaultLight(ZoneStateProvider zoneStateProvider, List<Power> lights)
-            throws IOException, ApiException {
-        CoolingZoneSetting zoneSetting = (CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting();
-
-        if (zoneSetting.getLight() != null && lights.contains(zoneSetting.getLight())) {
-            return zoneSetting.getLight();
-        }
-
-        return lights.get(0);
+        Power swing = ((CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting()).getSwing();
+        return (swing != null) && swings.contains(swing) ? swing : swings.get(0);
     }
 
     private ACFanLevel getCurrentOrDefaultFanLevel(ZoneStateProvider zoneStateProvider, List<ACFanLevel> fanLevels)
             throws IOException, ApiException {
-        CoolingZoneSetting zoneSetting = (CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting();
-
-        if (zoneSetting.getFanLevel() != null && fanLevels.contains(zoneSetting.getFanLevel())) {
-            return zoneSetting.getFanLevel();
-        }
+        ACFanLevel fanLevel = ((CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting()).getFanLevel();
+        return (fanLevel != null) && fanLevels.contains(fanLevel) ? fanLevel : fanLevels.get(0);
+    }
 
-        return fanLevels.get(0);
+    private ACVerticalSwing getCurrentOrDefaultVerticalSwing(ZoneStateProvider zoneStateProvider,
+            List<ACVerticalSwing> vertSwings) throws IOException, ApiException {
+        ACVerticalSwing vertSwing = ((CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting())
+                .getVerticalSwing();
+        return (vertSwing != null) && vertSwings.contains(vertSwing) ? vertSwing : vertSwings.get(0);
     }
 
     private ACHorizontalSwing getCurrentOrDefaultHorizontalSwing(ZoneStateProvider zoneStateProvider,
-            List<ACHorizontalSwing> horizontalSwings) throws IOException, ApiException {
-        CoolingZoneSetting zoneSetting = (CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting();
-
-        if (zoneSetting.getHorizontalSwing() != null && horizontalSwings.contains(zoneSetting.getHorizontalSwing())) {
-            return zoneSetting.getHorizontalSwing();
-        }
-
-        return horizontalSwings.get(0);
+            List<ACHorizontalSwing> horzSwings) throws IOException, ApiException {
+        ACHorizontalSwing horzSwing = ((CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting())
+                .getHorizontalSwing();
+        return (horzSwing != null) && horzSwings.contains(horzSwing) ? horzSwing : horzSwings.get(0);
     }
 
-    private ACVerticalSwing getCurrentOrDefaultVerticalSwing(ZoneStateProvider zoneStateProvider,
-            List<ACVerticalSwing> verticalSwings) throws IOException, ApiException {
-        CoolingZoneSetting zoneSetting = (CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting();
-
-        if (zoneSetting.getVerticalSwing() != null && verticalSwings.contains(zoneSetting.getVerticalSwing())) {
-            return zoneSetting.getVerticalSwing();
-        }
-
-        return verticalSwings.get(0);
+    private Power getCurrentOrDefaultLight(ZoneStateProvider zoneStateProvider, List<Power> lights)
+            throws IOException, ApiException {
+        Power light = ((CoolingZoneSetting) zoneStateProvider.getZoneState().getSetting()).getLight();
+        return (light != null) && lights.contains(light) ? light : lights.get(0);
     }
 
     private CoolingZoneSetting coolingSetting(boolean powerOn) {
index d6d35896571c9e1190246460d4b96919f81b6ccf..ef4997e1b74232be88ea1c0741148dfea1f8c4f1 100644 (file)
@@ -16,6 +16,7 @@ import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.temperature
 
 import java.io.IOException;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanSpeed;
 import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing;
@@ -34,6 +35,7 @@ import org.openhab.binding.tado.internal.api.model.TemperatureObject;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class HeatingZoneSettingsBuilder extends ZoneSettingsBuilder {
     private static final float DEFAULT_TEMPERATURE_C = 22.0f;
     private static final float DEFAULT_TEMPERATURE_F = 72.0f;
@@ -77,6 +79,7 @@ public class HeatingZoneSettingsBuilder extends ZoneSettingsBuilder {
 
         HeatingZoneSetting setting = heatingSetting(true);
 
+        Float temperature = this.temperature;
         if (temperature != null) {
             setting.setTemperature(temperature(temperature, temperatureUnit));
         }
index b4e9d7c77af302eced0080dd2674e4f1db7d0fb5..5961547918f280aafa92eefb26429fdc29ddc6ba 100644 (file)
@@ -16,6 +16,7 @@ import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.temperature
 
 import java.io.IOException;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanSpeed;
 import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing;
@@ -35,6 +36,7 @@ import org.openhab.binding.tado.internal.api.model.TemperatureObject;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class HotWaterZoneSettingsBuilder extends ZoneSettingsBuilder {
     private static final float DEFAULT_TEMPERATURE_C = 50.0f;
     private static final float DEFAULT_TEMPERATURE_F = 122.0f;
@@ -78,6 +80,7 @@ public class HotWaterZoneSettingsBuilder extends ZoneSettingsBuilder {
 
         HotWaterZoneSetting setting = hotWaterSetting(true);
 
+        Float temperature = this.temperature;
         if (temperature != null) {
             setting.setTemperature(temperature(temperature, temperatureUnit));
         }
index 3eaadd32d8c0dde81e4eaefe7815ea12d0786d26..a75273e51f767f9d9fdcf7b147a1d0ed3bc25d6f 100644 (file)
@@ -16,6 +16,8 @@ import static org.openhab.binding.tado.internal.api.TadoApiTypeUtils.*;
 
 import java.io.IOException;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.api.ApiException;
 import org.openhab.binding.tado.internal.api.model.OverlayTerminationCondition;
 import org.openhab.binding.tado.internal.api.model.OverlayTerminationConditionType;
@@ -28,11 +30,13 @@ import org.openhab.binding.tado.internal.handler.TadoZoneHandler;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TerminationConditionBuilder {
-    private TadoZoneHandler zoneHandler;
 
-    private OverlayTerminationConditionType terminationType = null;
-    private Integer timerDurationInSeconds = null;
+    private final TadoZoneHandler zoneHandler;
+
+    private @Nullable OverlayTerminationConditionType terminationType;
+    private int timerDurationInSeconds = 0;
 
     protected TerminationConditionBuilder(TadoZoneHandler zoneHandler) {
         this.zoneHandler = zoneHandler;
@@ -45,23 +49,23 @@ public class TerminationConditionBuilder {
     public TerminationConditionBuilder withTerminationType(OverlayTerminationConditionType terminationType) {
         this.terminationType = terminationType;
         if (terminationType != OverlayTerminationConditionType.TIMER) {
-            timerDurationInSeconds = null;
+            timerDurationInSeconds = 0;
         }
-
         return this;
     }
 
-    public TerminationConditionBuilder withTimerDurationInSeconds(Integer timerDurationInSeconds) {
+    public TerminationConditionBuilder withTimerDurationInSeconds(int timerDurationInSeconds) {
         this.terminationType = OverlayTerminationConditionType.TIMER;
         this.timerDurationInSeconds = timerDurationInSeconds;
         return this;
     }
 
     public OverlayTerminationCondition build(ZoneStateProvider zoneStateProvider) throws IOException, ApiException {
-        OverlayTerminationCondition terminationCondition = null;
+        OverlayTerminationCondition terminationCondition;
 
+        OverlayTerminationConditionType terminationType = this.terminationType;
         if (terminationType != null) {
-            if (terminationType != OverlayTerminationConditionType.TIMER || timerDurationInSeconds != null) {
+            if (terminationType != OverlayTerminationConditionType.TIMER || timerDurationInSeconds > 0) {
                 terminationCondition = getTerminationCondition(terminationType, timerDurationInSeconds);
             } else {
                 terminationCondition = getCurrentOrDefaultTimerTermination(zoneStateProvider);
@@ -75,18 +79,19 @@ public class TerminationConditionBuilder {
                 terminationCondition = getDefaultTerminationCondition();
             }
         }
+
         return terminationCondition;
     }
 
     private OverlayTerminationCondition getDefaultTerminationCondition() throws IOException, ApiException {
         OverlayTerminationCondition defaultTerminationCondition = zoneHandler.getDefaultTerminationCondition();
-        return defaultTerminationCondition != null ? defaultTerminationCondition : manualTermination();
+        return defaultTerminationCondition;
     }
 
     private TimerTerminationCondition getCurrentOrDefaultTimerTermination(ZoneStateProvider zoneStateProvider)
             throws IOException, ApiException {
         // Timer without duration
-        int duration = zoneHandler.getFallbackTimerDuration() * 60;
+        Integer duration = zoneHandler.getFallbackTimerDuration() * 60;
 
         ZoneState zoneState = zoneStateProvider.getZoneState();
 
index 43d7efd9991643cdd3585fa3b93ce391f5014df7..2f10418ac1645c38f849ae3088e374549455f058 100644 (file)
@@ -14,13 +14,15 @@ package org.openhab.binding.tado.internal.builder;
 
 import java.io.IOException;
 
-import org.openhab.binding.tado.internal.TadoBindingConstants;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanSpeed;
 import org.openhab.binding.tado.internal.TadoBindingConstants.HorizontalSwing;
 import org.openhab.binding.tado.internal.TadoBindingConstants.HvacMode;
 import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
 import org.openhab.binding.tado.internal.TadoBindingConstants.VerticalSwing;
+import org.openhab.binding.tado.internal.TadoBindingConstants.ZoneType;
 import org.openhab.binding.tado.internal.api.ApiException;
 import org.openhab.binding.tado.internal.api.model.GenericZoneCapabilities;
 import org.openhab.binding.tado.internal.api.model.GenericZoneSetting;
@@ -32,12 +34,12 @@ import org.openhab.binding.tado.internal.handler.TadoZoneHandler;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public abstract class ZoneSettingsBuilder {
+
     public static ZoneSettingsBuilder of(TadoZoneHandler zoneHandler) {
-        TadoBindingConstants.ZoneType zoneType = zoneHandler.getZoneType();
-        if (zoneType == null) {
-            throw new IllegalArgumentException("Zone type is null");
-        }
+        ZoneType zoneType = zoneHandler.getZoneType();
+
         switch (zoneType) {
             case HEATING:
                 return new HeatingZoneSettingsBuilder();
@@ -46,19 +48,19 @@ public abstract class ZoneSettingsBuilder {
             case HOT_WATER:
                 return new HotWaterZoneSettingsBuilder();
             default:
-                throw new IllegalArgumentException("Zone type " + zoneHandler.getZoneType() + " unknown");
+                throw new IllegalArgumentException("Zone type " + zoneType + " unknown");
         }
     }
 
-    protected HvacMode mode = null;
-    protected Float temperature = null;
     protected TemperatureUnit temperatureUnit = TemperatureUnit.CELSIUS;
-    protected Boolean swing = null;
-    protected Boolean light = null;
-    protected FanSpeed fanSpeed = null;
-    protected FanLevel fanLevel = null;
-    protected HorizontalSwing horizontalSwing = null;
-    protected VerticalSwing verticalSwing = null;
+    protected @Nullable Float temperature;
+    protected @Nullable HvacMode mode;
+    protected @Nullable Boolean swing;
+    protected @Nullable Boolean light;
+    protected @Nullable FanSpeed fanSpeed;
+    protected @Nullable FanLevel fanLevel;
+    protected @Nullable HorizontalSwing horizontalSwing;
+    protected @Nullable VerticalSwing verticalSwing;
 
     public ZoneSettingsBuilder withMode(HvacMode mode) {
         this.mode = mode;
@@ -100,10 +102,6 @@ public abstract class ZoneSettingsBuilder {
             throws IOException, ApiException;
 
     protected TemperatureObject truncateTemperature(TemperatureObject temperature) {
-        if (temperature == null) {
-            return null;
-        }
-
         TemperatureObject temperatureObject = new TemperatureObject();
         if (temperatureUnit == TemperatureUnit.FAHRENHEIT) {
             temperatureObject.setFahrenheit(temperature.getFahrenheit());
index ecf9a390e7c9554182a31c218a7aa9c5b456b126..ae19a841847f7773ecf3f901434bfce0bb437b6d 100644 (file)
@@ -14,6 +14,8 @@ package org.openhab.binding.tado.internal.builder;
 
 import java.io.IOException;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.api.ApiException;
 import org.openhab.binding.tado.internal.api.model.ZoneState;
 import org.openhab.binding.tado.internal.handler.TadoZoneHandler;
@@ -23,21 +25,20 @@ import org.openhab.binding.tado.internal.handler.TadoZoneHandler;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class ZoneStateProvider {
-    private TadoZoneHandler zoneHandler;
-    private ZoneState zoneState;
+    private final TadoZoneHandler zoneHandler;
+    private @Nullable ZoneState zoneState;
 
     public ZoneStateProvider(TadoZoneHandler zoneHandler) {
         this.zoneHandler = zoneHandler;
     }
 
-    ZoneState getZoneState() throws IOException, ApiException {
-        if (this.zoneState == null) {
-            ZoneState retrievedZoneState = zoneHandler.getZoneState();
-            // empty zone state behaves like a NULL object
-            this.zoneState = retrievedZoneState != null ? retrievedZoneState : new ZoneState();
+    public synchronized ZoneState getZoneState() throws IOException, ApiException {
+        ZoneState zoneState = this.zoneState;
+        if (zoneState == null) {
+            zoneState = this.zoneState = zoneHandler.getZoneState();
         }
-
-        return this.zoneState;
+        return zoneState;
     }
 }
index 230c04bfa56005e4ce0d0f5821f06c000617791f..1a9b9250acf81251d2ceb544db2d7af101b78ab4 100644 (file)
  */
 package org.openhab.binding.tado.internal.config;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * Holder-object for home configuration
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TadoHomeConfig {
-    public String username;
-    public String password;
+    public String username = "";
+    public String password = "";
 }
index f702fb575518d9a7e55551367fae11b8ec220e92..7e71b57b42d84ebf43672d6f1957087945cc36a1 100644 (file)
  */
 package org.openhab.binding.tado.internal.config;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * Holder-object for mobile device configuration
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TadoMobileDeviceConfig {
     public int id;
     public int refreshInterval;
index 467ed8021404e8a3f64dc01945198feff1c9d0fc..e5fa258d91e00aee89e66cc226312877d96a55f8 100644 (file)
  */
 package org.openhab.binding.tado.internal.config;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
 /**
  * Holder-object for zone configuration
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TadoZoneConfig {
     public long id;
     public int refreshInterval;
index 9de930c488189b88c5d27ff88eebe639ba68b8cc..64d18df58b130b72b0bac45dfc47e5d7a2cab00e 100644 (file)
@@ -25,6 +25,8 @@ import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.TadoBindingConstants;
 import org.openhab.binding.tado.internal.api.ApiException;
 import org.openhab.binding.tado.internal.api.model.MobileDevice;
@@ -43,13 +45,14 @@ import org.slf4j.LoggerFactory;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TadoDiscoveryService extends AbstractDiscoveryService {
     private static final int TIMEOUT = 5;
     private static final long REFRESH = 600;
 
     private final Logger logger = LoggerFactory.getLogger(TadoDiscoveryService.class);
 
-    private ScheduledFuture<?> discoveryFuture;
+    private @Nullable ScheduledFuture<?> discoveryFuture;
 
     public static final Set<ThingTypeUID> DISCOVERABLE_THING_TYPES_UIDS = Collections
             .unmodifiableSet(Stream.of(THING_TYPE_ZONE, THING_TYPE_MOBILE_DEVICE).collect(Collectors.toSet()));
@@ -83,23 +86,30 @@ public class TadoDiscoveryService extends AbstractDiscoveryService {
     @Override
     protected void startBackgroundDiscovery() {
         logger.debug("Start Tado background discovery");
+        ScheduledFuture<?> discoveryFuture = this.discoveryFuture;
         if (discoveryFuture == null || discoveryFuture.isCancelled()) {
             logger.debug("Start Scan");
-            discoveryFuture = scheduler.scheduleWithFixedDelay(this::startScan, 30, REFRESH, TimeUnit.SECONDS);
+            this.discoveryFuture = scheduler.scheduleWithFixedDelay(this::startScan, 30, REFRESH, TimeUnit.SECONDS);
         }
     }
 
     @Override
     protected void stopBackgroundDiscovery() {
         logger.debug("Stop Tado background discovery");
+        ScheduledFuture<?> discoveryFuture = this.discoveryFuture;
         if (discoveryFuture != null && !discoveryFuture.isCancelled()) {
             discoveryFuture.cancel(true);
-            discoveryFuture = null;
         }
     }
 
     private void discoverZones() {
         Long homeId = homeHandler.getHomeId();
+
+        if (homeId == null) {
+            logger.debug("Could not discover tado zones: Missing home id");
+            return;
+        }
+
         try {
             List<Zone> zoneList = homeHandler.getApi().listZones(homeId);
 
@@ -132,6 +142,12 @@ public class TadoDiscoveryService extends AbstractDiscoveryService {
 
     private void discoverMobileDevices() {
         Long homeId = homeHandler.getHomeId();
+
+        if (homeId == null) {
+            logger.debug("Could not discover mobile devices: Missing home id");
+            return;
+        }
+
         try {
             List<MobileDevice> mobileDeviceList = homeHandler.getApi().listMobileDevices(homeId);
 
@@ -143,7 +159,7 @@ public class TadoDiscoveryService extends AbstractDiscoveryService {
                 }
             }
         } catch (IOException | ApiException e) {
-            logger.debug("Could not discover tado zones: {}", e.getMessage(), e);
+            logger.debug("Could not discover mobile devices: {}", e.getMessage(), e);
         }
     }
 
index fc41c6bc1a2f02893cb231d6fe6a84c02a9838e4..b8231b0ba5fd6dcc81f0102a47059eb2e6427f39 100644 (file)
  */
 package org.openhab.binding.tado.internal.handler;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.api.client.HomeApi;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingStatus;
 import org.openhab.core.thing.binding.BaseThingHandler;
+import org.openhab.core.thing.binding.BridgeHandler;
 
 /**
  * Common base class for home-based thing-handler.
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public abstract class BaseHomeThingHandler extends BaseThingHandler {
 
     public BaseHomeThingHandler(Thing thing) {
         super(thing);
     }
 
-    public Long getHomeId() {
+    public @Nullable Long getHomeId() {
         TadoHomeHandler handler = getHomeHandler();
-        return handler != null ? handler.getHomeId() : Long.valueOf(0);
+        return handler.getHomeId();
     }
 
     protected TadoHomeHandler getHomeHandler() {
         Bridge bridge = getBridge();
-        return bridge != null ? (TadoHomeHandler) bridge.getHandler() : null;
+        if (bridge == null) {
+            throw new IllegalStateException("Bridge not initialized");
+        }
+        BridgeHandler handler = bridge.getHandler();
+        if (!(handler instanceof TadoHomeHandler)) {
+            throw new IllegalStateException("Handler not initialized");
+        }
+        return (TadoHomeHandler) handler;
     }
 
     protected HomeApi getApi() {
         TadoHomeHandler handler = getHomeHandler();
-        return handler != null ? handler.getApi() : null;
+        return handler.getApi();
     }
 
     protected void onSuccessfulOperation() {
index 856125477ef98100e0b864508053c8eba29e7118..d2edfad22723a584a2074cd1621aef3a63f3ed21 100644 (file)
@@ -19,6 +19,7 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
 
+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.core.library.types.OnOffType;
@@ -32,14 +33,16 @@ import org.slf4j.LoggerFactory;
  * devices.
  *
  * @author Andrew Fiddian-Green - Initial contribution
- * 
+ *
  */
+@NonNullByDefault
 public class TadoBatteryChecker {
     private final Logger logger = LoggerFactory.getLogger(TadoBatteryChecker.class);
 
-    private Map<Long, State> zoneList = new HashMap<>();
+    private final Map<Long, State> zoneList = new HashMap<>();
+    private final TadoHomeHandler homeHandler;
+
     private Date refreshTime = new Date();
-    private TadoHomeHandler homeHandler;
 
     public TadoBatteryChecker(TadoHomeHandler homeHandler) {
         this.homeHandler = homeHandler;
@@ -47,7 +50,7 @@ public class TadoBatteryChecker {
 
     private synchronized void refreshZoneList() {
         Date now = new Date();
-        if (homeHandler != null && (now.after(refreshTime) || zoneList.isEmpty())) {
+        if (now.after(refreshTime) || zoneList.isEmpty()) {
             // be frugal, we only need to refresh the battery state hourly
             Calendar calendar = Calendar.getInstance();
             calendar.setTime(now);
index f7ac2d6752dace4f0c58969ce55b26920d09b15f..8cdbdea3754da4b9fa1de8beb67563075dc6f0cb 100644 (file)
@@ -22,6 +22,8 @@ import java.util.Hashtable;
 import java.util.Map;
 import java.util.Set;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.discovery.TadoDiscoveryService;
 import org.openhab.core.config.discovery.DiscoveryService;
 import org.openhab.core.thing.Bridge;
@@ -42,6 +44,7 @@ import org.osgi.service.component.annotations.Reference;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 @Component(configurationPid = "binding.tado", service = ThingHandlerFactory.class)
 public class TadoHandlerFactory extends BaseThingHandlerFactory {
 
@@ -63,7 +66,7 @@ public class TadoHandlerFactory extends BaseThingHandlerFactory {
     }
 
     @Override
-    protected ThingHandler createHandler(Thing thing) {
+    protected @Nullable ThingHandler createHandler(Thing thing) {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
 
         if (thingTypeUID.equals(THING_TYPE_HOME)) {
index 3cffe4bfded25c0fea622f440d3721cdeaace5cc..e7064327a0e22d0fa17cbab4e61bf770ba015f72 100644 (file)
 package org.openhab.binding.tado.internal.handler;
 
 import java.io.IOException;
+import java.util.List;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.TadoBindingConstants;
 import org.openhab.binding.tado.internal.TadoBindingConstants.TemperatureUnit;
 import org.openhab.binding.tado.internal.api.ApiException;
@@ -26,6 +29,7 @@ import org.openhab.binding.tado.internal.api.model.HomePresence;
 import org.openhab.binding.tado.internal.api.model.HomeState;
 import org.openhab.binding.tado.internal.api.model.PresenceState;
 import org.openhab.binding.tado.internal.api.model.User;
+import org.openhab.binding.tado.internal.api.model.UserHomes;
 import org.openhab.binding.tado.internal.config.TadoHomeConfig;
 import org.openhab.core.library.types.OnOffType;
 import org.openhab.core.thing.Bridge;
@@ -36,6 +40,7 @@ import org.openhab.core.thing.binding.BaseBridgeHandler;
 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;
 
@@ -44,21 +49,23 @@ import org.slf4j.LoggerFactory;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TadoHomeHandler extends BaseBridgeHandler {
 
     private Logger logger = LoggerFactory.getLogger(TadoHomeHandler.class);
 
     private TadoHomeConfig configuration;
-    private HomeApi api;
-    private Long homeId;
+    private final HomeApi api;
 
-    private TadoBatteryChecker batteryChecker;
-
-    private ScheduledFuture<?> initializationFuture;
+    private @Nullable Long homeId;
+    private @Nullable TadoBatteryChecker batteryChecker;
+    private @Nullable ScheduledFuture<?> initializationFuture;
 
     public TadoHomeHandler(Bridge bridge) {
         super(bridge);
         batteryChecker = new TadoBatteryChecker(this);
+        configuration = getConfigAs(TadoHomeConfig.class);
+        api = new HomeApiFactory().create(configuration.username, configuration.password);
     }
 
     public TemperatureUnit getTemperatureUnit() {
@@ -70,11 +77,10 @@ public class TadoHomeHandler extends BaseBridgeHandler {
     @Override
     public void initialize() {
         configuration = getConfigAs(TadoHomeConfig.class);
-        api = new HomeApiFactory().create(configuration.username, configuration.password);
-
-        if (this.initializationFuture == null || this.initializationFuture.isDone()) {
-            initializationFuture = scheduler.scheduleWithFixedDelay(this::initializeBridgeStatusAndPropertiesIfOffline,
-                    0, 300, TimeUnit.SECONDS);
+        ScheduledFuture<?> initializationFuture = this.initializationFuture;
+        if (initializationFuture == null || initializationFuture.isDone()) {
+            this.initializationFuture = scheduler.scheduleWithFixedDelay(
+                    this::initializeBridgeStatusAndPropertiesIfOffline, 0, 300, TimeUnit.SECONDS);
         }
     }
 
@@ -93,13 +99,20 @@ public class TadoHomeHandler extends BaseBridgeHandler {
                 return;
             }
 
-            if (user.getHomes().isEmpty()) {
+            List<UserHomes> homes = user.getHomes();
+            if (homes == null || homes.isEmpty()) {
                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
                         "User does not have access to any home");
                 return;
             }
 
-            homeId = user.getHomes().get(0).getId().longValue();
+            Integer firstHomeId = homes.get(0).getId();
+            if (firstHomeId == null) {
+                updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Missing Home Id");
+                return;
+            }
+
+            homeId = firstHomeId.longValue();
 
             HomeInfo homeInfo = api.showHome(homeId);
             TemperatureUnit temperatureUnit = org.openhab.binding.tado.internal.api.model.TemperatureUnit.FAHRENHEIT == homeInfo
@@ -118,9 +131,9 @@ public class TadoHomeHandler extends BaseBridgeHandler {
     @Override
     public void dispose() {
         super.dispose();
-        if (this.initializationFuture != null || !this.initializationFuture.isDone()) {
-            this.initializationFuture.cancel(true);
-            this.initializationFuture = null;
+        ScheduledFuture<?> initializationFuture = this.initializationFuture;
+        if (initializationFuture != null && !initializationFuture.isCancelled()) {
+            initializationFuture.cancel(true);
         }
     }
 
@@ -128,13 +141,12 @@ public class TadoHomeHandler extends BaseBridgeHandler {
         return api;
     }
 
-    public Long getHomeId() {
+    public @Nullable Long getHomeId() {
         return homeId;
     }
 
     public HomeState getHomeState() throws IOException, ApiException {
-        HomeApi api = getApi();
-        return api != null ? api.homeState(getHomeId()) : null;
+        return api.homeState(getHomeId());
     }
 
     public void updateHomeState() {
@@ -173,6 +185,7 @@ public class TadoHomeHandler extends BaseBridgeHandler {
     }
 
     public State getBatteryLowAlarm(long zoneId) {
-        return batteryChecker.getBatteryLowAlarm(zoneId);
+        TadoBatteryChecker batteryChecker = this.batteryChecker;
+        return batteryChecker != null ? batteryChecker.getBatteryLowAlarm(zoneId) : UnDefType.UNDEF;
     }
 }
index 81c46717c95e68ca3bd722db4aa85253f6b6e87e..38b34ac34a3736137b60e55de400e28c68d8dce3 100644 (file)
@@ -16,6 +16,8 @@ import java.io.IOException;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.TadoBindingConstants;
 import org.openhab.binding.tado.internal.api.ApiException;
 import org.openhab.binding.tado.internal.api.model.MobileDevice;
@@ -37,15 +39,17 @@ import org.slf4j.LoggerFactory;
  *
  * @author Dennis Frommknecht - Initial contribution
  */
+@NonNullByDefault
 public class TadoMobileDeviceHandler extends BaseHomeThingHandler {
 
     private Logger logger = LoggerFactory.getLogger(TadoMobileDeviceHandler.class);
 
     private TadoMobileDeviceConfig configuration;
-    private ScheduledFuture<?> refreshTimer;
+    private @Nullable ScheduledFuture<?> refreshTimer;
 
     public TadoMobileDeviceHandler(Thing thing) {
         super(thing);
+        configuration = getConfigAs(TadoMobileDeviceConfig.class);
     }
 
     @Override
@@ -61,7 +65,6 @@ public class TadoMobileDeviceHandler extends BaseHomeThingHandler {
     @Override
     public void initialize() {
         configuration = getConfigAs(TadoMobileDeviceConfig.class);
-
         if (configuration.refreshInterval <= 0) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Refresh interval of zone "
                     + configuration.id + " of home " + getHomeId() + " must be greater than zero");
@@ -135,13 +138,15 @@ public class TadoMobileDeviceHandler extends BaseHomeThingHandler {
     }
 
     private void scheduleZoneStateUpdate() {
+        ScheduledFuture<?> refreshTimer = this.refreshTimer;
         if (refreshTimer == null || refreshTimer.isCancelled()) {
-            refreshTimer = scheduler.scheduleWithFixedDelay(this::updateState, 5, configuration.refreshInterval,
+            this.refreshTimer = scheduler.scheduleWithFixedDelay(this::updateState, 5, configuration.refreshInterval,
                     TimeUnit.SECONDS);
         }
     }
 
     private void cancelScheduledStateUpdate() {
+        ScheduledFuture<?> refreshTimer = this.refreshTimer;
         if (refreshTimer != null) {
             refreshTimer.cancel(false);
         }
index e76aa515eadbdb87d5a4ef8a4d6551e9bea6cf40..ce30cb37d6e85eb8ab53ed512ba436800bf9ba2c 100644 (file)
@@ -22,6 +22,7 @@ import java.util.stream.Collectors;
 
 import javax.measure.quantity.Temperature;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.openhab.binding.tado.internal.TadoBindingConstants;
 import org.openhab.binding.tado.internal.TadoBindingConstants.FanLevel;
@@ -33,13 +34,13 @@ import org.openhab.binding.tado.internal.TadoBindingConstants.ZoneType;
 import org.openhab.binding.tado.internal.TadoHvacChange;
 import org.openhab.binding.tado.internal.adapter.TadoZoneStateAdapter;
 import org.openhab.binding.tado.internal.api.ApiException;
+import org.openhab.binding.tado.internal.api.GsonBuilderFactory;
 import org.openhab.binding.tado.internal.api.TadoApiTypeUtils;
-import org.openhab.binding.tado.internal.api.client.HomeApi;
 import org.openhab.binding.tado.internal.api.model.ACFanLevel;
 import org.openhab.binding.tado.internal.api.model.ACHorizontalSwing;
 import org.openhab.binding.tado.internal.api.model.ACVerticalSwing;
+import org.openhab.binding.tado.internal.api.model.AcMode;
 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.CoolingZoneSetting;
 import org.openhab.binding.tado.internal.api.model.GenericZoneCapabilities;
 import org.openhab.binding.tado.internal.api.model.GenericZoneSetting;
@@ -65,11 +66,12 @@ import org.openhab.core.thing.ThingStatusDetail;
 import org.openhab.core.thing.ThingStatusInfo;
 import org.openhab.core.types.Command;
 import org.openhab.core.types.RefreshType;
-import org.openhab.core.types.State;
 import org.openhab.core.types.StateOption;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.gson.Gson;
+
 /**
  * The {@link TadoZoneHandler} is responsible for handling commands of zones and update their state.
  *
@@ -77,47 +79,61 @@ import org.slf4j.LoggerFactory;
  * @author Andrew Fiddian-Green - Added Low Battery Alarm, A/C Power and Open Window channels
  *
  */
+@NonNullByDefault
 public class TadoZoneHandler extends BaseHomeThingHandler {
     private Logger logger = LoggerFactory.getLogger(TadoZoneHandler.class);
 
     private final TadoStateDescriptionProvider stateDescriptionProvider;
-
     private TadoZoneConfig configuration;
-    private ScheduledFuture<?> refreshTimer;
-    private ScheduledFuture<?> scheduledHvacChange;
-    private GenericZoneCapabilities capabilities;
-    TadoHvacChange pendingHvacChange;
+
+    private @Nullable ScheduledFuture<?> refreshTimer;
+    private @Nullable ScheduledFuture<?> scheduledHvacChange;
+    private @Nullable GenericZoneCapabilities capabilities;
+    private @Nullable TadoHvacChange pendingHvacChange;
+
+    private boolean disposing = false;
+    private @Nullable Gson gson;
 
     public TadoZoneHandler(Thing thing, TadoStateDescriptionProvider stateDescriptionProvider) {
         super(thing);
         this.stateDescriptionProvider = stateDescriptionProvider;
+        configuration = getConfigAs(TadoZoneConfig.class);
     }
 
     public long getZoneId() {
-        return this.configuration.id;
+        return configuration.id;
     }
 
     public int getFallbackTimerDuration() {
-        return this.configuration.fallbackTimerDuration;
+        return configuration.fallbackTimerDuration;
     }
 
-    public @Nullable ZoneType getZoneType() {
-        String zoneTypeStr = this.thing.getProperties().get(TadoBindingConstants.PROPERTY_ZONE_TYPE);
-        return zoneTypeStr != null ? ZoneType.valueOf(zoneTypeStr) : null;
+    public ZoneType getZoneType() {
+        String zoneTypeStr = thing.getProperties().get(TadoBindingConstants.PROPERTY_ZONE_TYPE);
+        if (zoneTypeStr == null) {
+            throw new IllegalStateException("Zone type not initialized");
+        }
+        return ZoneType.valueOf(zoneTypeStr);
     }
 
     public OverlayTerminationCondition getDefaultTerminationCondition() throws IOException, ApiException {
         OverlayTemplate overlayTemplate = getApi().showZoneDefaultOverlay(getHomeId(), getZoneId());
+        logApiTransaction(overlayTemplate, false);
         return terminationConditionTemplateToTerminationCondition(overlayTemplate.getTerminationCondition());
     }
 
     public ZoneState getZoneState() throws IOException, ApiException {
-        HomeApi api = getApi();
-        return api != null ? api.showZoneState(getHomeId(), getZoneId()) : null;
+        ZoneState zoneState = getApi().showZoneState(getHomeId(), getZoneId());
+        logApiTransaction(zoneState, false);
+        return zoneState;
     }
 
     public GenericZoneCapabilities getZoneCapabilities() {
-        return this.capabilities;
+        GenericZoneCapabilities capabilities = this.capabilities;
+        if (capabilities == null) {
+            throw new IllegalStateException("Zone capabilities not initialized");
+        }
+        return capabilities;
     }
 
     public TemperatureUnit getTemperatureUnit() {
@@ -125,9 +141,17 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
     }
 
     public Overlay setOverlay(Overlay overlay) throws IOException, ApiException {
-        logger.debug("Setting overlay of home {} and zone {} with overlay: {}", getHomeId(), getZoneId(),
-                overlay.toString());
-        return getApi().updateZoneOverlay(getHomeId(), getZoneId(), overlay);
+        try {
+            logApiTransaction(overlay, true);
+            Overlay newOverlay = getApi().updateZoneOverlay(getHomeId(), getZoneId(), overlay);
+            logApiTransaction(newOverlay, false);
+            return newOverlay;
+        } catch (ApiException e) {
+            if (!logger.isTraceEnabled()) {
+                logger.warn("ApiException sending JSON content:\n{}", convertToJsonString(overlay));
+            }
+            throw e;
+        }
     }
 
     public void removeOverlay() throws IOException, ApiException {
@@ -144,65 +168,75 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
             return;
         }
 
-        switch (id) {
-            case TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE:
-                pendingHvacChange.withHvacMode(((StringType) command).toFullString());
-                scheduleHvacChange();
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE:
-                QuantityType<Temperature> state = (QuantityType<Temperature>) command;
-                QuantityType<Temperature> stateInTargetUnit = getTemperatureUnit() == TemperatureUnit.FAHRENHEIT
-                        ? state.toUnit(ImperialUnits.FAHRENHEIT)
-                        : state.toUnit(SIUnits.CELSIUS);
-
-                if (stateInTargetUnit != null) {
-                    pendingHvacChange.withTemperature(stateInTargetUnit.floatValue());
-                    scheduleHvacChange();
-                }
+        synchronized (this) {
+            TadoHvacChange pendingHvacChange = this.pendingHvacChange;
+            if (pendingHvacChange == null) {
+                throw new IllegalStateException("Zone pendingHvacChange not initialized");
+            }
 
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_SWING:
-                pendingHvacChange.withSwing(((OnOffType) command) == OnOffType.ON);
-                scheduleHvacChange();
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_LIGHT:
-                pendingHvacChange.withLight(((OnOffType) command) == OnOffType.ON);
-                scheduleHvacChange();
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED:
-                pendingHvacChange.withFanSpeed(((StringType) command).toFullString());
-                scheduleHvacChange();
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL:
-                String fanLevelString = ((StringType) command).toFullString();
-                pendingHvacChange.withFanLevel(FanLevel.valueOf(fanLevelString.toUpperCase()));
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING:
-                String horizontalSwingString = ((StringType) command).toFullString();
-                pendingHvacChange.withHorizontalSwing(HorizontalSwing.valueOf(horizontalSwingString.toUpperCase()));
-                scheduleHvacChange();
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING:
-                String verticalSwingString = ((StringType) command).toFullString();
-                pendingHvacChange.withVerticalSwing(VerticalSwing.valueOf(verticalSwingString.toUpperCase()));
-                scheduleHvacChange();
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE:
-                String operationMode = ((StringType) command).toFullString();
-                pendingHvacChange.withOperationMode(OperationMode.valueOf(operationMode));
-                scheduleHvacChange();
-                break;
-            case TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION:
-                pendingHvacChange.activeFor(((DecimalType) command).intValue());
-                scheduleHvacChange();
-                break;
+            switch (id) {
+                case TadoBindingConstants.CHANNEL_ZONE_HVAC_MODE:
+                    pendingHvacChange.withHvacMode(((StringType) command).toFullString());
+                    scheduleHvacChange();
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_TARGET_TEMPERATURE:
+                    if (command instanceof QuantityType<?>) {
+                        @SuppressWarnings("unchecked")
+                        QuantityType<Temperature> state = (QuantityType<Temperature>) command;
+                        QuantityType<Temperature> stateInTargetUnit = getTemperatureUnit() == TemperatureUnit.FAHRENHEIT
+                                ? state.toUnit(ImperialUnits.FAHRENHEIT)
+                                : state.toUnit(SIUnits.CELSIUS);
+
+                        if (stateInTargetUnit != null) {
+                            pendingHvacChange.withTemperature(stateInTargetUnit.floatValue());
+                            scheduleHvacChange();
+                        }
+                    }
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_SWING:
+                    pendingHvacChange.withSwing(((OnOffType) command) == OnOffType.ON);
+                    scheduleHvacChange();
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_LIGHT:
+                    pendingHvacChange.withLight(((OnOffType) command) == OnOffType.ON);
+                    scheduleHvacChange();
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_FAN_SPEED:
+                    pendingHvacChange.withFanSpeed(((StringType) command).toFullString());
+                    scheduleHvacChange();
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL:
+                    String fanLevelString = ((StringType) command).toFullString();
+                    pendingHvacChange.withFanLevel(FanLevel.valueOf(fanLevelString.toUpperCase()));
+                    scheduleHvacChange();
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING:
+                    String horizontalSwingString = ((StringType) command).toFullString();
+                    pendingHvacChange.withHorizontalSwing(HorizontalSwing.valueOf(horizontalSwingString.toUpperCase()));
+                    scheduleHvacChange();
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING:
+                    String verticalSwingString = ((StringType) command).toFullString();
+                    pendingHvacChange.withVerticalSwing(VerticalSwing.valueOf(verticalSwingString.toUpperCase()));
+                    scheduleHvacChange();
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE:
+                    String operationMode = ((StringType) command).toFullString();
+                    pendingHvacChange.withOperationMode(OperationMode.valueOf(operationMode));
+                    scheduleHvacChange();
+                    break;
+                case TadoBindingConstants.CHANNEL_ZONE_TIMER_DURATION:
+                    pendingHvacChange.activeForMinutes(((DecimalType) command).intValue());
+                    scheduleHvacChange();
+                    break;
+            }
         }
     }
 
     @Override
     public void initialize() {
+        disposing = false;
         configuration = getConfigAs(TadoZoneConfig.class);
-
         if (configuration.refreshInterval <= 0) {
             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Refresh interval of zone "
                     + getZoneId() + " of home " + getHomeId() + " must be greater than zero");
@@ -225,6 +259,7 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
 
     @Override
     public void dispose() {
+        disposing = true;
         cancelScheduledZoneStateUpdate();
     }
 
@@ -233,7 +268,10 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
         if (bridgeStatusInfo.getStatus() == ThingStatus.ONLINE) {
             try {
                 Zone zoneDetails = getApi().showZoneDetails(getHomeId(), getZoneId());
+                logApiTransaction(zoneDetails, false);
+
                 GenericZoneCapabilities capabilities = getApi().showZoneCapabilities(getHomeId(), getZoneId());
+                logApiTransaction(capabilities, false);
 
                 if (zoneDetails == null || capabilities == null) {
                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
@@ -244,7 +282,6 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
                 updateProperty(TadoBindingConstants.PROPERTY_ZONE_NAME, zoneDetails.getName());
                 updateProperty(TadoBindingConstants.PROPERTY_ZONE_TYPE, zoneDetails.getType().name());
                 this.capabilities = capabilities;
-                logger.debug("Got capabilities: {}", capabilities.toString());
             } catch (IOException | ApiException e) {
                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
                         "Could not connect to server due to " + e.getMessage());
@@ -263,12 +300,14 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
     }
 
     private void updateZoneState(boolean forceUpdate) {
-        TadoHomeHandler home = getHomeHandler();
-        if (home != null) {
-            home.updateHomeState();
+        if ((thing.getStatus() != ThingStatus.ONLINE) || disposing) {
+            return;
         }
 
+        getHomeHandler().updateHomeState();
+
         // No update during HVAC change debounce
+        ScheduledFuture<?> scheduledHvacChange = this.scheduledHvacChange;
         if (!forceUpdate && scheduledHvacChange != null && !scheduledHvacChange.isDone()) {
             return;
         }
@@ -276,18 +315,14 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
         try {
             ZoneState zoneState = getZoneState();
 
-            if (zoneState == null) {
-                return;
-            }
-
             logger.debug("Updating state of home {} and zone {}", getHomeId(), getZoneId());
 
             TadoZoneStateAdapter state = new TadoZoneStateAdapter(zoneState, getTemperatureUnit());
-            updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE, state.getInsideTemperature());
-            updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_HUMIDITY, state.getHumidity());
+            updateState(TadoBindingConstants.CHANNEL_ZONE_CURRENT_TEMPERATURE, state.getInsideTemperature());
+            updateState(TadoBindingConstants.CHANNEL_ZONE_HUMIDITY, state.getHumidity());
 
-            updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER, state.getHeatingPower());
-            updateStateIfNotNull(TadoBindingConstants.CHANNEL_ZONE_AC_POWER, state.getAcPower());
+            updateState(TadoBindingConstants.CHANNEL_ZONE_HEATING_POWER, state.getHeatingPower());
+            updateState(TadoBindingConstants.CHANNEL_ZONE_AC_POWER, state.getAcPower());
 
             updateState(TadoBindingConstants.CHANNEL_ZONE_OPERATION_MODE, state.getOperationMode());
 
@@ -314,9 +349,8 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
                     "Could not connect to server due to " + e.getMessage());
         }
 
-        if (home != null) {
-            updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM, home.getBatteryLowAlarm(getZoneId()));
-        }
+        updateState(TadoBindingConstants.CHANNEL_ZONE_BATTERY_LOW_ALARM,
+                getHomeHandler().getBatteryLowAlarm(getZoneId()));
     }
 
     /**
@@ -333,32 +367,35 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
             return;
         }
 
-        AcModeCapabilities acCapabilities = TadoApiTypeUtils.getModeCapabilities(
-                (AirConditioningCapabilities) capabilities, ((CoolingZoneSetting) setting).getMode());
+        AcMode acMode = ((CoolingZoneSetting) setting).getMode();
+        AcModeCapabilities acModeCapabilities = acMode == null ? new AcModeCapabilities()
+                : TadoApiTypeUtils.getModeCapabilities(acMode, capabilities);
 
-        if (acCapabilities != null) {
-            Channel channel;
-
-            // update the options list of supported fan levels
-            channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL);
-            List<ACFanLevel> fanLevels = acCapabilities.getFanLevel();
-            if (channel != null && fanLevels != null) {
+        // update the options list of supported fan levels
+        Channel channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_FAN_LEVEL);
+        if (channel != null) {
+            List<ACFanLevel> fanLevels = acModeCapabilities.getFanLevel();
+            if (fanLevels != null) {
                 stateDescriptionProvider.setStateOptions(channel.getUID(),
                         fanLevels.stream().map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
             }
+        }
 
-            // update the options list of supported horizontal swing settings
-            channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING);
-            List<ACHorizontalSwing> horizontalSwings = acCapabilities.getHorizontalSwing();
-            if (channel != null && horizontalSwings != null) {
+        // update the options list of supported horizontal swing settings
+        channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_HORIZONTAL_SWING);
+        if (channel != null) {
+            List<ACHorizontalSwing> horizontalSwings = acModeCapabilities.getHorizontalSwing();
+            if (horizontalSwings != null) {
                 stateDescriptionProvider.setStateOptions(channel.getUID(), horizontalSwings.stream()
                         .map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
             }
+        }
 
-            // update the options list of supported vertical swing settings
-            channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING);
-            List<ACVerticalSwing> verticalSwings = acCapabilities.getVerticalSwing();
-            if (channel != null && verticalSwings != null) {
+        // update the options list of supported vertical swing settings
+        channel = thing.getChannel(TadoBindingConstants.CHANNEL_ZONE_VERTICAL_SWING);
+        if (channel != null) {
+            List<ACVerticalSwing> verticalSwings = acModeCapabilities.getVerticalSwing();
+            if (verticalSwings != null) {
                 stateDescriptionProvider.setStateOptions(channel.getUID(), verticalSwings.stream()
                         .map(u -> new StateOption(u.name(), u.name())).collect(Collectors.toList()));
             }
@@ -366,8 +403,9 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
     }
 
     private void scheduleZoneStateUpdate() {
+        ScheduledFuture<?> refreshTimer = this.refreshTimer;
         if (refreshTimer == null || refreshTimer.isCancelled()) {
-            refreshTimer = scheduler.scheduleWithFixedDelay(new Runnable() {
+            this.refreshTimer = scheduler.scheduleWithFixedDelay(new Runnable() {
                 @Override
                 public void run() {
                     updateZoneState(false);
@@ -377,21 +415,26 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
     }
 
     private void cancelScheduledZoneStateUpdate() {
+        ScheduledFuture<?> refreshTimer = this.refreshTimer;
         if (refreshTimer != null) {
             refreshTimer.cancel(false);
         }
     }
 
     private void scheduleHvacChange() {
+        ScheduledFuture<?> scheduledHvacChange = this.scheduledHvacChange;
         if (scheduledHvacChange != null) {
             scheduledHvacChange.cancel(false);
         }
-
-        scheduledHvacChange = scheduler.schedule(() -> {
+        this.scheduledHvacChange = scheduler.schedule(() -> {
             try {
-                TadoHvacChange change = this.pendingHvacChange;
-                this.pendingHvacChange = new TadoHvacChange(getThing());
-                change.apply();
+                synchronized (this) {
+                    TadoHvacChange pendingHvacChange = this.pendingHvacChange;
+                    this.pendingHvacChange = new TadoHvacChange(getThing());
+                    if (pendingHvacChange != null) {
+                        pendingHvacChange.apply();
+                    }
+                }
             } catch (IOException e) {
                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
             } catch (ApiException e) {
@@ -403,9 +446,32 @@ public class TadoZoneHandler extends BaseHomeThingHandler {
         }, configuration.hvacChangeDebounce, TimeUnit.SECONDS);
     }
 
-    private void updateStateIfNotNull(String channelID, State state) {
-        if (state != null) {
-            updateState(channelID, state);
+    /**
+     * Helper method to log an API transaction on the given object.
+     * If the logger level is 'debug', the transaction is simply logged.
+     * If the logger level is 'trace, the object's JSON serial contents are included.
+     *
+     * @param object the object to be logged.
+     * @param isCommand marks whether the transaction is a command to, or a response from, the server.
+     */
+    private void logApiTransaction(Object object, boolean isCommand) {
+        if (logger.isDebugEnabled() || logger.isTraceEnabled()) {
+            String logType = isCommand ? "command" : "response";
+            if (logger.isTraceEnabled()) {
+                logger.trace("Api {}: homeId:{}, zoneId:{}, objectId:{}, content:\n{}", logType, getHomeId(),
+                        getZoneId(), object.getClass().getSimpleName(), convertToJsonString(object));
+            } else if (logger.isDebugEnabled()) {
+                logger.debug("Api {}: homeId:{}, zoneId:{}, objectId:{}", logType, getHomeId(), getZoneId(),
+                        object.getClass().getSimpleName());
+            }
+        }
+    }
+
+    private synchronized String convertToJsonString(Object object) {
+        Gson gson = this.gson;
+        if (gson == null) {
+            gson = this.gson = GsonBuilderFactory.defaultGsonBuilder().setPrettyPrinting().create();
         }
+        return gson.toJson(object);
     }
 }