diff --git a/bundles/org.openhab.binding.hue/doc/readme_v2.md b/bundles/org.openhab.binding.hue/doc/readme_v2.md index 5041b89b91b49..cdd32afac0ec2 100644 --- a/bundles/org.openhab.binding.hue/doc/readme_v2.md +++ b/bundles/org.openhab.binding.hue/doc/readme_v2.md @@ -59,30 +59,32 @@ The configuration of all things (as described above) is the same regardless of w Device things support some of the following channels: -| Channel ID | Item Type | Description | -|-----------------------|--------------------|---------------------------------------------------------------------------------------------------------------------| -| color | Color | Supports full color control with hue, saturation and brightness values, or brightness only, or switching on or off. | -| brightness | Dimmer | Supports control of the brightness value, or switching on or off. | -| color-temperature | Dimmer | Supports control of the color temperature in percent from cold (0%) to warm (100%). | -| color-temperature-abs | Number:Temperature | Supports control of the color temperature via a QuantityType having a temperature unit e.g. Kelvin. (Advanced) | -| switch | Switch | Supports switching the device on and off. | -| dynamics | Number:Time | Sets the duration of dynamic transitions between light states. (Advanced) | -| alert | String | Allows setting an alert on a light e.g. flashing them. (Advanced) | -| effect | String | Allows setting an effect on a light e.g. 'candle' effect. (Advanced) | -| button-last-event | (String) | Informs which button was last pressed in the device. (Trigger Channel) | -| rotary-steps | (String) | Informs about the number of rotary steps of the last rotary dial movement. (Trigger Channel) | -| motion | Switch | Shows if motion has been detected by the sensor. (Read Only) | -| motion-enabled | Switch | Supports enabling / disabling the motion sensor. (Advanced) | -| light-level | Number:Illuminance | Shows the current light level measured by the sensor. (Read Only) | -| light-level-enabled | Switch | Supports enabling / disabling the light level sensor. (Advanced) | -| temperature | Number:Temperature | Shows the current temperature measured by the sensor. (Read Only) | -| temperature-enabled | Switch | Supports enabling / disabling the temperature sensor. (Advanced) | -| battery-level | Number | Shows the battery level. (Read Only) | -| battery-low | Switch | Indicates whether the battery is low or not. (Read Only) | -| last-updated | DateTime | The date and time when the thing state was last updated. (Read Only) (Advanced) | -| color-xy-only | Color | Allows access to the `color-xy` parameter of the light(s) only. Has no impact on `dimming` or `on-off` parameters. | -| dimming-only | Dimmer | Allows access to the `dimming` parameter of the light(s) only. Has no impact on `color-xy` or `on-off` parameters. | -| on-off-only | Switch | Allows access to the `on-off` parameter of the light(s) only. Has no impact on `color-xy` or `dimming` parameters. | +| Channel ID | Item Type | Description | +|--------------------------|--------------------|---------------------------------------------------------------------------------------------------------------------| +| color | Color | Supports full color control with hue, saturation and brightness values, or brightness only, or switching on or off. | +| brightness | Dimmer | Supports control of the brightness value, or switching on or off. | +| color-temperature | Dimmer | Supports control of the color temperature in percent from cold (0%) to warm (100%). | +| color-temperature-abs | Number:Temperature | Supports control of the color temperature via a QuantityType having a temperature unit e.g. Kelvin. (Advanced) | +| switch | Switch | Supports switching the device on and off. | +| dynamics | Number:Time | Sets the duration of dynamic transitions between light states. (Advanced) | +| alert | String | Allows setting an alert on a light e.g. flashing them. (Advanced) | +| effect | String | Allows setting an effect on a light e.g. 'candle' effect. (Advanced) | +| button-last-event | (String) | Informs which button was last pressed in the device. (Trigger Channel) | +| rotary-steps | (String) | Informs about the number of rotary steps of the last rotary dial movement. (Trigger Channel) | +| motion | Switch | Shows if motion has been detected by the sensor. (Read Only) | +| motion-enabled | Switch | Supports enabling / disabling the motion sensor. (Advanced) | +| motion-last-updated | DateTime | The date and time when the motion value was last updated. (Read Only) (Advanced) | +| light-level | Number:Illuminance | Shows the current light level measured by the sensor. (Read Only) | +| light-level-enabled | Switch | Supports enabling / disabling the light level sensor. (Advanced) | +| temperature | Number:Temperature | Shows the current temperature measured by the sensor. (Read Only) | +| temperature-last-updated | DateTime | The date and time when the temperature was last updated. (Read Only) (Advanced) | +| temperature-enabled | Switch | Supports enabling / disabling the temperature sensor. (Advanced) | +| battery-level | Number | Shows the battery level. (Read Only) | +| battery-low | Switch | Indicates whether the battery is low or not. (Read Only) | +| last-updated | DateTime | The date and time when the thing state was last updated. (Read Only) (Advanced) | +| color-xy-only | Color | Allows access to the `color-xy` parameter of the light(s) only. Has no impact on `dimming` or `on-off` parameters. | +| dimming-only | Dimmer | Allows access to the `dimming` parameter of the light(s) only. Has no impact on `color-xy` or `on-off` parameters. | +| on-off-only | Switch | Allows access to the `on-off` parameter of the light(s) only. Has no impact on `color-xy` or `dimming` parameters. | The exact list of channels in a given device is determined at run time when the system is started. Each device reports its own live list of capabilities, and the respective list of channels is created accordingly. diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java index 83c6ac420d1c3..93b4c8de02489 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/HueBindingConstants.java @@ -153,10 +153,12 @@ public class HueBindingConstants { public static final String CHANNEL_2_BUTTON_LAST_EVENT = "button-last-event"; public static final String CHANNEL_2_ROTARY_STEPS = "rotary-steps"; public static final String CHANNEL_2_MOTION = "motion"; + public static final String CHANNEL_2_MOTION_LAST_UPDATED = "motion-last-updated"; public static final String CHANNEL_2_MOTION_ENABLED = "motion-enabled"; public static final String CHANNEL_2_LIGHT_LEVEL = "light-level"; public static final String CHANNEL_2_LIGHT_LEVEL_ENABLED = "light-level-enabled"; public static final String CHANNEL_2_TEMPERATURE = CHANNEL_TEMPERATURE; + public static final String CHANNEL_2_TEMPERATURE_LAST_UPDATED = "temperature-last-updated"; public static final String CHANNEL_2_TEMPERATURE_ENABLED = "temperature-enabled"; public static final String CHANNEL_2_BATTERY_LEVEL = "battery-level"; public static final String CHANNEL_2_BATTERY_LOW = "battery-low"; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java index dc60d6e1827d0..ad76b049d25fa 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/connection/Clip2Bridge.java @@ -81,12 +81,14 @@ import org.openhab.binding.hue.internal.exceptions.ApiException; import org.openhab.binding.hue.internal.exceptions.HttpUnauthorizedException; import org.openhab.binding.hue.internal.handler.Clip2BridgeHandler; +import org.openhab.binding.hue.internal.serialization.InstantDeserializer; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.io.net.http.HttpUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonParseException; @@ -556,7 +558,8 @@ public static boolean isClip2Supported(String hostName) throws IOException { private final String registrationUrl; private final String applicationKey; private final Clip2BridgeHandler bridgeHandler; - private final Gson jsonParser = new Gson(); + private final Gson jsonParser = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()) + .create(); private final Semaphore streamMutex = new Semaphore(MAX_CONCURRENT_STREAMS, true); // i.e. fair private final ReadWriteLock sessionUseCreateLock = new ReentrantReadWriteLock(true); // i.e. fair private final Map> fatalErrorTasks = new ConcurrentHashMap<>(); diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Motion.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Motion.java index a669af0281a5e..3b0ce60c89f06 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Motion.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Motion.java @@ -13,6 +13,7 @@ package org.openhab.binding.hue.internal.dto.clip2; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.types.OnOffType; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; @@ -28,11 +29,24 @@ public class Motion { private boolean motion; private @SerializedName("motion_valid") boolean motionValid; + private @Nullable @SerializedName("motion_report") MotionReport motionReport; + /** + * The underlying field is deprecated in the CLIP 2 API. + * Moved to motion_report/motion. + * Should be used only as fallback for older firmwares. + * + * @return true if motion is detected + */ public boolean isMotion() { return motion; } + /** + * The underlying field is deprecated in the CLIP 2 API. + * Motion is valid when motion_report property is present, invalid when absent. + * Should be used only as fallback for older firmwares. + */ public boolean isMotionValid() { return motionValid; } @@ -44,4 +58,8 @@ public State getMotionState() { public State getMotionValidState() { return OnOffType.from(motionValid); } + + public @Nullable MotionReport getMotionReport() { + return motionReport; + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MotionReport.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MotionReport.java new file mode 100644 index 0000000000000..d0b84fcf3d026 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/MotionReport.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * DTO for CLIP 2 motion sensor report. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class MotionReport { + private @Nullable Instant changed; + private boolean motion; + + /** + * @return last time the value of this property is changed. + */ + public @Nullable Instant getLastChanged() { + return changed; + } + + /** + * @return true if motion is detected + */ + public boolean isMotion() { + return motion; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java index b749da3c9bd70..003b91c49be50 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Resource.java @@ -16,6 +16,9 @@ import java.math.MathContext; import java.math.RoundingMode; import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; import java.util.List; import java.util.Map; import java.util.Objects; @@ -31,12 +34,14 @@ import org.openhab.binding.hue.internal.dto.clip2.enums.SmartSceneState; import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus; import org.openhab.binding.hue.internal.exceptions.DTOPresentButEmptyException; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; import org.openhab.core.types.Command; import org.openhab.core.types.State; import org.openhab.core.types.UnDefType; @@ -410,7 +415,30 @@ public State getLightLevelState() { public State getMotionState() { Motion motion = this.motion; - return Objects.nonNull(motion) ? motion.getMotionState() : UnDefType.NULL; + if (motion == null) { + return UnDefType.NULL; + } + MotionReport motionReport = motion.getMotionReport(); + if (motionReport == null) { + return motion.getMotionState(); + } + return OnOffType.from(motionReport.isMotion()); + } + + public State getMotionLastUpdatedState(ZoneId zoneId) { + Motion motion = this.motion; + if (motion == null) { + return UnDefType.NULL; + } + MotionReport motionReport = motion.getMotionReport(); + if (motionReport == null) { + return UnDefType.UNDEF; + } + Instant lastChanged = motionReport.getLastChanged(); + if (lastChanged == null) { + return UnDefType.UNDEF; + } + return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId)); } public State getMotionValidState() { @@ -559,7 +587,30 @@ public JsonObject getStatus() { public State getTemperatureState() { Temperature temperature = this.temperature; - return Objects.nonNull(temperature) ? temperature.getTemperatureState() : UnDefType.NULL; + if (temperature == null) { + return UnDefType.NULL; + } + TemperatureReport temperatureReport = temperature.getTemperatureReport(); + if (temperatureReport == null) { + return temperature.getTemperatureState(); + } + return new QuantityType<>(temperatureReport.getTemperature(), SIUnits.CELSIUS); + } + + public State getTemperatureLastUpdatedState(ZoneId zoneId) { + Temperature temperature = this.temperature; + if (temperature == null) { + return UnDefType.NULL; + } + TemperatureReport temperatureReport = temperature.getTemperatureReport(); + if (temperatureReport == null) { + return UnDefType.UNDEF; + } + Instant lastChanged = temperatureReport.getLastChanged(); + if (lastChanged == null) { + return UnDefType.UNDEF; + } + return new DateTimeType(ZonedDateTime.ofInstant(lastChanged, zoneId)); } public State getTemperatureValidState() { diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Temperature.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Temperature.java index 8f25c2b08a403..401476a70a798 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Temperature.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/Temperature.java @@ -13,6 +13,7 @@ package org.openhab.binding.hue.internal.dto.clip2; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.unit.SIUnits; @@ -30,11 +31,24 @@ public class Temperature { private float temperature; private @SerializedName("temperature_valid") boolean temperatureValid; + private @Nullable @SerializedName("temperature_report") TemperatureReport temperatureReport; + /** + * The underlying field is deprecated in the CLIP 2 API. + * Moved to temperature_report/temperature. + * Should be used only as fallback for older firmwares. + * + * @return temperature in 1.00 degrees Celsius + */ public float getTemperature() { return temperature; } + /** + * The underlying field is deprecated in the CLIP 2 API. + * Indication whether the value presented in temperature is valid + * Should be used only as fallback for older firmwares. + */ public boolean isTemperatureValid() { return temperatureValid; } @@ -46,4 +60,8 @@ public State getTemperatureState() { public State getTemperatureValidState() { return OnOffType.from(temperatureValid); } + + public @Nullable TemperatureReport getTemperatureReport() { + return temperatureReport; + } } diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TemperatureReport.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TemperatureReport.java new file mode 100644 index 0000000000000..9c3db81dec8df --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/dto/clip2/TemperatureReport.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.dto.clip2; + +import java.time.Instant; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * DTO for CLIP 2 temperature sensor report. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class TemperatureReport { + private @Nullable Instant changed; + private float temperature; + + /** + * @return last time the value of this property is changed. + */ + public @Nullable Instant getLastChanged() { + return changed; + } + + /** + * @return temperature in 1.00 degrees Celsius + */ + public float getTemperature() { + return temperature; + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/factory/HueThingHandlerFactory.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/factory/HueThingHandlerFactory.java index 64f06484026e0..13583c8306350 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/factory/HueThingHandlerFactory.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/factory/HueThingHandlerFactory.java @@ -37,6 +37,7 @@ import org.openhab.binding.hue.internal.handler.sensors.TemperatureHandler; import org.openhab.core.config.core.Configuration; import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.i18n.TranslationProvider; import org.openhab.core.io.net.http.HttpClientFactory; import org.openhab.core.thing.Bridge; @@ -82,6 +83,7 @@ public class HueThingHandlerFactory extends BaseThingHandlerFactory { private final Clip2StateDescriptionProvider clip2StateDescriptionProvider; private final TranslationProvider i18nProvider; private final LocaleProvider localeProvider; + private final TimeZoneProvider timeZoneProvider; private final ThingRegistry thingRegistry; private final ItemChannelLinkRegistry itemChannelLinkRegistry; @@ -90,13 +92,14 @@ public HueThingHandlerFactory(final @Reference HttpClientFactory httpClientFacto final @Reference HueStateDescriptionProvider stateDescriptionProvider, final @Reference Clip2StateDescriptionProvider clip2StateDescriptionProvider, final @Reference TranslationProvider i18nProvider, final @Reference LocaleProvider localeProvider, - final @Reference ThingRegistry thingRegistry, + final @Reference TimeZoneProvider timeZoneProvider, final @Reference ThingRegistry thingRegistry, final @Reference ItemChannelLinkRegistry itemChannelLinkRegistry) { this.httpClientFactory = httpClientFactory; this.stateDescriptionProvider = stateDescriptionProvider; this.clip2StateDescriptionProvider = clip2StateDescriptionProvider; this.i18nProvider = i18nProvider; this.localeProvider = localeProvider; + this.timeZoneProvider = timeZoneProvider; this.thingRegistry = thingRegistry; this.itemChannelLinkRegistry = itemChannelLinkRegistry; } @@ -184,7 +187,8 @@ private ThingUID getThingUID(ThingTypeUID thingTypeUID, String id, @Nullable Thi return new Clip2BridgeHandler((Bridge) thing, httpClientFactory, thingRegistry, localeProvider, i18nProvider); } else if (Clip2ThingHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { - return new Clip2ThingHandler(thing, clip2StateDescriptionProvider, thingRegistry, itemChannelLinkRegistry); + return new Clip2ThingHandler(thing, clip2StateDescriptionProvider, timeZoneProvider, thingRegistry, + itemChannelLinkRegistry); } else if (HueBridgeHandler.SUPPORTED_THING_TYPES.contains(thingTypeUID)) { return new HueBridgeHandler((Bridge) thing, httpClientFactory.getCommonHttpClient(), stateDescriptionProvider, i18nProvider, localeProvider); diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java index bab93b79beba0..9ef318045faba 100644 --- a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/handler/Clip2ThingHandler.java @@ -57,6 +57,7 @@ import org.openhab.binding.hue.internal.dto.clip2.helper.Setters; import org.openhab.binding.hue.internal.exceptions.ApiException; import org.openhab.binding.hue.internal.exceptions.AssetNotLoadedException; +import org.openhab.core.i18n.TimeZoneProvider; import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; @@ -160,6 +161,7 @@ public class Clip2ThingHandler extends BaseThingHandler { private final ThingRegistry thingRegistry; private final ItemChannelLinkRegistry itemChannelLinkRegistry; private final Clip2StateDescriptionProvider stateDescriptionProvider; + private final TimeZoneProvider timeZoneProvider; private String resourceId = "?"; private Resource thisResource; @@ -180,7 +182,8 @@ public class Clip2ThingHandler extends BaseThingHandler { private @Nullable Future updateServiceContributorsTask; public Clip2ThingHandler(Thing thing, Clip2StateDescriptionProvider stateDescriptionProvider, - ThingRegistry thingRegistry, ItemChannelLinkRegistry itemChannelLinkRegistry) { + TimeZoneProvider timeZoneProvider, ThingRegistry thingRegistry, + ItemChannelLinkRegistry itemChannelLinkRegistry) { super(thing); ThingTypeUID thingTypeUID = thing.getThingTypeUID(); @@ -197,6 +200,7 @@ public Clip2ThingHandler(Thing thing, Clip2StateDescriptionProvider stateDescrip this.thingRegistry = thingRegistry; this.itemChannelLinkRegistry = itemChannelLinkRegistry; this.stateDescriptionProvider = stateDescriptionProvider; + this.timeZoneProvider = timeZoneProvider; } /** @@ -870,6 +874,8 @@ private boolean updateChannels(Resource resource) { case MOTION: updateState(CHANNEL_2_MOTION, resource.getMotionState(), fullUpdate); + updateState(CHANNEL_2_MOTION_LAST_UPDATED, + resource.getMotionLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate); updateState(CHANNEL_2_MOTION_ENABLED, resource.getEnabledState(), fullUpdate); break; @@ -883,6 +889,8 @@ private boolean updateChannels(Resource resource) { case TEMPERATURE: updateState(CHANNEL_2_TEMPERATURE, resource.getTemperatureState(), fullUpdate); + updateState(CHANNEL_2_TEMPERATURE_LAST_UPDATED, + resource.getTemperatureLastUpdatedState(timeZoneProvider.getTimeZone()), fullUpdate); updateState(CHANNEL_2_TEMPERATURE_ENABLED, resource.getEnabledState(), fullUpdate); break; diff --git a/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/serialization/InstantDeserializer.java b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/serialization/InstantDeserializer.java new file mode 100644 index 0000000000000..1843c3c0417eb --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/java/org/openhab/binding/hue/internal/serialization/InstantDeserializer.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.binding.hue.internal.serialization; + +import java.lang.reflect.Type; +import java.time.Instant; +import java.time.format.DateTimeParseException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; + +/** + * The {@link InstantDeserializer} converts a formatted UTC string to {@link Instant}. + * + * @author Jacob Laursen - Initial contribution + */ +@NonNullByDefault +public class InstantDeserializer implements JsonDeserializer { + + @Override + public @Nullable Instant deserialize(JsonElement element, Type arg1, JsonDeserializationContext arg2) + throws JsonParseException { + String content = element.getAsString(); + try { + return Instant.parse(content); + } catch (DateTimeParseException e) { + throw new JsonParseException("Could not parse as Instant: " + content, e); + } + } +} diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties index b79fd4ddbd2ac..34ef315b44396 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/i18n/hue.properties @@ -46,10 +46,14 @@ thing-type.hue.device.channel.effect.description = Activate the effect for the l thing-type.hue.device.channel.light-level.description = Current light level. thing-type.hue.device.channel.light-level-enabled.description = Light level sensor enabled. thing-type.hue.device.channel.motion-enabled.description = Motion sensor enabled. +thing-type.hue.device.channel.motion-last-updated.label = Motion Last Updated +thing-type.hue.device.channel.motion-last-updated.description = The date and time when the motion value was last updated. thing-type.hue.device.channel.on-off-only.description = Set the on/off parameter of the light without changing other state parameters. thing-type.hue.device.channel.temperature.label = Temperature thing-type.hue.device.channel.temperature.description = Temperature at the sensor location. thing-type.hue.device.channel.temperature-enabled.description = Temperature sensor enabled. +thing-type.hue.device.channel.temperature-last-updated.label = Temperature Last Updated +thing-type.hue.device.channel.temperature-last-updated.description = The date and time when the temperature was last updated. thing-type.hue.geofencesensor.label = Geofence Sensor thing-type.hue.geofencesensor.description = A sensor providing geofence based presence detection. thing-type.hue.group.label = Hue Group diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Clip2Thing.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Clip2Thing.xml index d6e768fad2397..e680d4ea8c996 100644 --- a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Clip2Thing.xml +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/thing/Clip2Thing.xml @@ -26,6 +26,10 @@ + + + The date and time when the motion value was last updated. + Motion sensor enabled. @@ -39,6 +43,10 @@ Temperature at the sensor location. + + + The date and time when the temperature was last updated. + Temperature sensor enabled. @@ -58,6 +66,10 @@ + + 1 + + resourceId diff --git a/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/update/instructions.xml b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/update/instructions.xml new file mode 100644 index 0000000000000..9efee9add5ad1 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/main/resources/OH-INF/update/instructions.xml @@ -0,0 +1,21 @@ + + + + + + + + hue:last-updated-v2 + + + + hue:last-updated-v2 + + + + + + + diff --git a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java index 05a3ffed8864e..affe44b050b14 100644 --- a/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java +++ b/bundles/org.openhab.binding.hue/src/test/java/org/openhab/binding/hue/internal/clip2/Clip2DtoTest.java @@ -19,6 +19,8 @@ import java.io.IOException; import java.lang.reflect.Field; import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; import java.util.List; import java.util.Optional; import java.util.Set; @@ -55,6 +57,8 @@ import org.openhab.binding.hue.internal.dto.clip2.enums.RotationEventType; import org.openhab.binding.hue.internal.dto.clip2.enums.ZigbeeStatus; import org.openhab.binding.hue.internal.dto.clip2.helper.Setters; +import org.openhab.binding.hue.internal.serialization.InstantDeserializer; +import org.openhab.core.library.types.DateTimeType; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.HSBType; import org.openhab.core.library.types.OnOffType; @@ -66,6 +70,7 @@ import org.openhab.core.util.ColorUtil; import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -79,7 +84,8 @@ @NonNullByDefault class Clip2DtoTest { - private static final Gson GSON = new Gson(); + private static final Gson GSON = new GsonBuilder().registerTypeAdapter(Instant.class, new InstantDeserializer()) + .create(); private static final Double MINIMUM_DIMMING_LEVEL = Double.valueOf(12.34f); /** @@ -493,6 +499,24 @@ void testSensor2Motion() { Boolean enabled = item.getEnabled(); assertNotNull(enabled); assertTrue(enabled); + assertEquals(OnOffType.ON, item.getMotionState()); + assertEquals(new DateTimeType("2023-09-04T20:04:30.395+0000"), + item.getMotionLastUpdatedState(ZoneId.of("UTC"))); + } + + @Test + void testSensor2MotionDeprecated() { + String json = load(ResourceType.MOTION.name().toLowerCase() + "_deprecated"); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(1, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.MOTION, item.getType()); + Boolean enabled = item.getEnabled(); + assertNotNull(enabled); + assertTrue(enabled); Motion motion = item.getMotion(); assertNotNull(motion); assertTrue(motion.isMotion()); @@ -563,6 +587,27 @@ void testTemperature() { assertEquals(1, list.size()); Resource item = list.get(0); assertEquals(ResourceType.TEMPERATURE, item.getType()); + Boolean enabled = item.getEnabled(); + assertNotNull(enabled); + assertTrue(enabled); + assertEquals(QuantityType.valueOf("23.34 °C"), item.getTemperatureState()); + assertEquals(new DateTimeType("2023-09-06T18:22:07.016+0000"), + item.getTemperatureLastUpdatedState(ZoneId.of("UTC"))); + } + + @Test + void testTemperatureDeprecated() { + String json = load(ResourceType.TEMPERATURE.name().toLowerCase() + "_deprecated"); + Resources resources = GSON.fromJson(json, Resources.class); + assertNotNull(resources); + List list = resources.getResources(); + assertNotNull(list); + assertEquals(1, list.size()); + Resource item = list.get(0); + assertEquals(ResourceType.TEMPERATURE, item.getType()); + Boolean enabled = item.getEnabled(); + assertNotNull(enabled); + assertTrue(enabled); Temperature temperature = item.getTemperature(); assertNotNull(temperature); assertEquals(17.2, temperature.getTemperature(), 0.1); diff --git a/bundles/org.openhab.binding.hue/src/test/resources/motion.json b/bundles/org.openhab.binding.hue/src/test/resources/motion.json index b5b7ba658dc39..f18b91abba846 100644 --- a/bundles/org.openhab.binding.hue/src/test/resources/motion.json +++ b/bundles/org.openhab.binding.hue/src/test/resources/motion.json @@ -1,19 +1,28 @@ { - "errors": [], - "data": [ - { - "id": "97244487-dd25-4f4c-b829-8eb5ffb82c29", - "id_v1": "/sensors/30", - "owner": { - "rid": "70660557-692d-4d37-8b6b-e3ec63716a72", - "rtype": "device" - }, - "enabled": true, - "motion": { - "motion": true, - "motion_valid": true - }, - "type": "motion" - } - ] -} \ No newline at end of file + "errors": [], + "data": [ + { + "id": "00000000-0000-0000-0000-000000000005", + "id_v1": "/sensors/5", + "owner": { + "rid": "00000000-0000-0000-0000-000000000000", + "rtype": "device" + }, + "enabled": true, + "motion": { + "motion": false, + "motion_valid": true, + "motion_report": { + "changed": "2023-09-04T20:04:30.395Z", + "motion": true + } + }, + "sensitivity": { + "status": "set", + "sensitivity": 2, + "sensitivity_max": 4 + }, + "type": "motion" + } + ] +} diff --git a/bundles/org.openhab.binding.hue/src/test/resources/motion_deprecated.json b/bundles/org.openhab.binding.hue/src/test/resources/motion_deprecated.json new file mode 100644 index 0000000000000..b5b7ba658dc39 --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/motion_deprecated.json @@ -0,0 +1,19 @@ +{ + "errors": [], + "data": [ + { + "id": "97244487-dd25-4f4c-b829-8eb5ffb82c29", + "id_v1": "/sensors/30", + "owner": { + "rid": "70660557-692d-4d37-8b6b-e3ec63716a72", + "rtype": "device" + }, + "enabled": true, + "motion": { + "motion": true, + "motion_valid": true + }, + "type": "motion" + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.hue/src/test/resources/temperature.json b/bundles/org.openhab.binding.hue/src/test/resources/temperature.json index 5034d6aee816d..464bc9e1894ea 100644 --- a/bundles/org.openhab.binding.hue/src/test/resources/temperature.json +++ b/bundles/org.openhab.binding.hue/src/test/resources/temperature.json @@ -1,19 +1,23 @@ { - "errors": [], - "data": [ - { - "id": "f5f8010d-8356-4604-88dc-4f73912cbb6a", - "id_v1": "/sensors/33", - "owner": { - "rid": "70660557-692d-4d37-8b6b-e3ec63716a72", - "rtype": "device" - }, - "enabled": true, - "temperature": { - "temperature": 17.26, - "temperature_valid": true - }, - "type": "temperature" - } - ] -} \ No newline at end of file + "errors": [], + "data": [ + { + "id": "00000000-0000-0000-0000-000000000004", + "id_v1": "/sensors/5", + "owner": { + "rid": "00000000-0000-0000-0000-000000000000", + "rtype": "device" + }, + "enabled": true, + "temperature": { + "temperature": 23.35, + "temperature_valid": true, + "temperature_report": { + "changed": "2023-09-06T18:22:07.016Z", + "temperature": 23.34 + } + }, + "type": "temperature" + } + ] +} diff --git a/bundles/org.openhab.binding.hue/src/test/resources/temperature_deprecated.json b/bundles/org.openhab.binding.hue/src/test/resources/temperature_deprecated.json new file mode 100644 index 0000000000000..5034d6aee816d --- /dev/null +++ b/bundles/org.openhab.binding.hue/src/test/resources/temperature_deprecated.json @@ -0,0 +1,19 @@ +{ + "errors": [], + "data": [ + { + "id": "f5f8010d-8356-4604-88dc-4f73912cbb6a", + "id_v1": "/sensors/33", + "owner": { + "rid": "70660557-692d-4d37-8b6b-e3ec63716a72", + "rtype": "device" + }, + "enabled": true, + "temperature": { + "temperature": 17.26, + "temperature_valid": true + }, + "type": "temperature" + } + ] +} \ No newline at end of file