diff --git a/bundles/org.openhab.binding.netatmo/README.md b/bundles/org.openhab.binding.netatmo/README.md index 3bece02857a..9fa7a8fac7a 100644 --- a/bundles/org.openhab.binding.netatmo/README.md +++ b/bundles/org.openhab.binding.netatmo/README.md @@ -83,12 +83,12 @@ Once authentication process has been done, current refreshToken is stored in `/O | presence | Thing | NOC | The Netatmo Smart Outdoor Camera (Presence) camera with or without siren. | id, ipAddress | | siren | Thing | NIS | The Netatmo Smart Indoor Siren. | id | | doorbell | Thing | NDB | The Netatmo Smart Video Doorbell device. | id, ipAddress | -| weather-station | Bridge | NAMain | Main indoor module reporting temperature, humidity, pressure, air quality and sound level. | id | +| weather-station | Bridge | NAMain | Main indoor module reporting temperature, humidity, pressure, air quality and sound level. | id, refreshInterval | | outdoor | Thing | NAModule1 | Outdoor module reporting temperature and humidity. | id | | wind | Thing | NAModule2 | Wind sensor reporting wind angle and strength. | id | | rain | Thing | NAModule3 | Rain Gauge measuring precipitation. | id | | indoor | Thing | NAModule4 | Additional indoor module reporting temperature, humidity and CO2 level. | id | -| home-coach | Thing | NHC | Healthy home coach reporting health-index, temperature, humidity, pressure, air quality, sound level. | id | +| home-coach | Thing | NHC | Healthy home coach reporting health-index, temperature, humidity, pressure, air quality, sound level. | id, refreshInterval | | plug | Thing | NAPlug | The relay connected to the boiler controlling a Thermostat and zero or more valves. | id | | thermostat | Thing | NATherm1 | The Thermostat device placed in a given room. | id | | room | Thing | NARoom | A room in your house. | id | @@ -161,8 +161,8 @@ If you did not manually create things in the *.things file, the Netatmo Binding ### Weather Station Main Indoor Device -Weather station does not need any refreshInterval setting. -Based on a standard update period of 10mn by Netatmo systems - it will auto adapt to stick closest as possible to last data availability. +Weather station uses a default `refreshInterval` of 10 minutes (can be adjusted), based on a standard update period of Netatmo systems. +It will auto-adapt to stick as closely as possible to the last data availability. **Supported channels for the main indoor module:** @@ -330,6 +330,9 @@ All these channels are read only. ### Healthy Home Coach Device +Home Coach uses a default `refreshInterval` of 10 minutes (can be adjusted), based on a standard update period of Netatmo systems. +It will auto-adapt to stick as closely as possible to the last data availability. + **Supported channels for the healthy home coach device:** | Channel Group | Channel Id | Item Type | Description | @@ -453,11 +456,12 @@ Depending on the way it is configured the behaviour will be adapted and availabl The Home thing has the following configuration elements: -| Parameter | Type | Required | Description | -| ---------- | ------ | -------- | ----------------------------------------------------------------------------------- | -| id (1) | String | No | If you have a single type of equipment, this id is to be used for the home | -| energyId | String | No | Id of a home holding energy control devices | -| securityId | String | No | Id of a home holding security monitoring devices | +| Parameter | Type | Required | Description | +| --------------- | ------- | -------- | ----------------------------------------------------------------------------------- | +| id (1) | String | No | If you have a single type of equipment, this id is to be used for the home | +| energyId | String | No | Id of a home holding energy control devices | +| securityId | String | No | Id of a home holding security monitoring devices | +| refreshInterval | Integer | No | Refresh interval for refreshing the data in seconds. Default 180. | At least one of these parameter must be filled - at most two : @@ -465,7 +469,7 @@ At least one of these parameter must be filled - at most two : * id or energyId * securityId and energyId -(1) this parameter is only kept for backward compatibility. +(1) this parameter is kept for backward compatibility. All channels are read only. diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java index 20e704292f7..9af2f0ad269 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/NetatmoHandlerFactory.java @@ -36,8 +36,11 @@ import org.openhab.binding.netatmo.internal.handler.capability.DeviceCapability; import org.openhab.binding.netatmo.internal.handler.capability.DoorbellCapability; import org.openhab.binding.netatmo.internal.handler.capability.HomeCapability; import org.openhab.binding.netatmo.internal.handler.capability.MeasureCapability; +import org.openhab.binding.netatmo.internal.handler.capability.ParentUpdateCapability; import org.openhab.binding.netatmo.internal.handler.capability.PersonCapability; import org.openhab.binding.netatmo.internal.handler.capability.PresenceCapability; +import org.openhab.binding.netatmo.internal.handler.capability.RefreshAutoCapability; +import org.openhab.binding.netatmo.internal.handler.capability.RefreshCapability; import org.openhab.binding.netatmo.internal.handler.capability.RoomCapability; import org.openhab.binding.netatmo.internal.handler.capability.WeatherCapability; import org.openhab.binding.netatmo.internal.handler.channelhelper.ChannelHelper; @@ -148,6 +151,12 @@ public class NetatmoHandlerFactory extends BaseThingHandlerFactory { newCap = new MeasureCapability(handler, helpers); } else if (capability == ChannelHelperCapability.class) { newCap = new ChannelHelperCapability(handler, helpers); + } else if (capability == RefreshAutoCapability.class) { + newCap = new RefreshAutoCapability(handler); + } else if (capability == RefreshCapability.class) { + newCap = new RefreshCapability(handler); + } else if (capability == ParentUpdateCapability.class) { + newCap = new ParentUpdateCapability(handler); } if (newCap != null) { handler.getCapabilities().put(newCap); diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java index a1b201fb86f..b1f4cbadf03 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/api/data/ModuleType.java @@ -34,8 +34,11 @@ import org.openhab.binding.netatmo.internal.handler.capability.DeviceCapability; import org.openhab.binding.netatmo.internal.handler.capability.DoorbellCapability; import org.openhab.binding.netatmo.internal.handler.capability.HomeCapability; import org.openhab.binding.netatmo.internal.handler.capability.MeasureCapability; +import org.openhab.binding.netatmo.internal.handler.capability.ParentUpdateCapability; import org.openhab.binding.netatmo.internal.handler.capability.PersonCapability; import org.openhab.binding.netatmo.internal.handler.capability.PresenceCapability; +import org.openhab.binding.netatmo.internal.handler.capability.RefreshAutoCapability; +import org.openhab.binding.netatmo.internal.handler.capability.RefreshCapability; import org.openhab.binding.netatmo.internal.handler.capability.RoomCapability; import org.openhab.binding.netatmo.internal.handler.capability.WeatherCapability; import org.openhab.binding.netatmo.internal.handler.channelhelper.AirQualityChannelHelper; @@ -70,91 +73,100 @@ public enum ModuleType { new ChannelGroup(ApiBridgeChannelHelper.class, GROUP_MONITORING)), HOME(FeatureArea.NONE, "NAHome", 1, "home", ACCOUNT, - Set.of(DeviceCapability.class, HomeCapability.class, ChannelHelperCapability.class), + Set.of(DeviceCapability.class, HomeCapability.class, ChannelHelperCapability.class, + RefreshCapability.class), new ChannelGroup(SecurityChannelHelper.class, GROUP_SECURITY_EVENT, GROUP_SECURITY), new ChannelGroup(EnergyChannelHelper.class, GROUP_ENERGY)), PERSON(FeatureArea.SECURITY, "NAPerson", 1, "virtual", HOME, - Set.of(PersonCapability.class, ChannelHelperCapability.class), + Set.of(PersonCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), new ChannelGroup(PersonChannelHelper.class, GROUP_PERSON), new ChannelGroup(EventPersonChannelHelper.class, GROUP_PERSON_LAST_EVENT)), WELCOME(FeatureArea.SECURITY, "NACamera", 1, "camera", HOME, - Set.of(CameraCapability.class, ChannelHelperCapability.class), ChannelGroup.SIGNAL, ChannelGroup.EVENT, + Set.of(CameraCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), + ChannelGroup.SIGNAL, ChannelGroup.EVENT, new ChannelGroup(CameraChannelHelper.class, GROUP_SECURITY_EVENT, GROUP_CAM_STATUS, GROUP_CAM_LIVE)), - TAG(FeatureArea.SECURITY, "NACamDoorTag", 1, "device", WELCOME, Set.of(ChannelHelperCapability.class), - ChannelGroup.SIGNAL, ChannelGroup.BATTERY, ChannelGroup.TIMESTAMP, - new ChannelGroup(DoorTagChannelHelper.class, GROUP_TAG)), + TAG(FeatureArea.SECURITY, "NACamDoorTag", 1, "device", WELCOME, + Set.of(ChannelHelperCapability.class, ParentUpdateCapability.class), ChannelGroup.SIGNAL, + ChannelGroup.BATTERY, ChannelGroup.TIMESTAMP, new ChannelGroup(DoorTagChannelHelper.class, GROUP_TAG)), - SIREN(FeatureArea.SECURITY, "NIS", 1, "device", WELCOME, Set.of(ChannelHelperCapability.class), ChannelGroup.SIGNAL, + SIREN(FeatureArea.SECURITY, "NIS", 1, "device", WELCOME, + Set.of(ChannelHelperCapability.class, ParentUpdateCapability.class), ChannelGroup.SIGNAL, ChannelGroup.BATTERY, ChannelGroup.TIMESTAMP, new ChannelGroup(SirenChannelHelper.class, GROUP_SIREN)), PRESENCE(FeatureArea.SECURITY, "NOC", 1, "camera", HOME, - Set.of(PresenceCapability.class, ChannelHelperCapability.class), ChannelGroup.SIGNAL, ChannelGroup.EVENT, + Set.of(PresenceCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), + ChannelGroup.SIGNAL, ChannelGroup.EVENT, new ChannelGroup(PresenceChannelHelper.class, GROUP_SECURITY_EVENT, GROUP_CAM_STATUS, GROUP_CAM_LIVE, GROUP_PRESENCE), new ChannelGroup(EventCameraChannelHelper.class, GROUP_SUB_EVENT)), DOORBELL(FeatureArea.SECURITY, "NDB", 1, "camera", HOME, - Set.of(DoorbellCapability.class, ChannelHelperCapability.class), ChannelGroup.SIGNAL, + Set.of(DoorbellCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), + ChannelGroup.SIGNAL, new ChannelGroup(CameraChannelHelper.class, GROUP_SECURITY_EVENT, GROUP_DOORBELL_STATUS, GROUP_DOORBELL_LIVE), new ChannelGroup(EventCameraChannelHelper.class, GROUP_DOORBELL_LAST_EVENT, GROUP_DOORBELL_SUB_EVENT)), - WEATHER_STATION(FeatureArea.WEATHER, "NAMain", 1, "device", ACCOUNT, + WEATHER_STATION(FeatureArea.WEATHER, "NAMain", 1, "weather", ACCOUNT, Set.of(DeviceCapability.class, WeatherCapability.class, MeasureCapability.class, - ChannelHelperCapability.class), + ChannelHelperCapability.class, RefreshAutoCapability.class), ChannelGroup.SIGNAL, ChannelGroup.HUMIDITY, ChannelGroup.TSTAMP_EXT, ChannelGroup.MEASURE, ChannelGroup.AIR_QUALITY, ChannelGroup.LOCATION, ChannelGroup.NOISE, ChannelGroup.TEMP_INSIDE_EXT, new ChannelGroup(PressureChannelHelper.class, MeasureClass.PRESSURE, GROUP_TYPE_PRESSURE_EXTENDED)), OUTDOOR(FeatureArea.WEATHER, "NAModule1", 1, "device", WEATHER_STATION, - Set.of(MeasureCapability.class, ChannelHelperCapability.class), ChannelGroup.SIGNAL, ChannelGroup.HUMIDITY, - ChannelGroup.TSTAMP_EXT, ChannelGroup.MEASURE, ChannelGroup.BATTERY, ChannelGroup.TEMP_OUTSIDE_EXT), + Set.of(MeasureCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), + ChannelGroup.SIGNAL, ChannelGroup.HUMIDITY, ChannelGroup.TSTAMP_EXT, ChannelGroup.MEASURE, + ChannelGroup.BATTERY, ChannelGroup.TEMP_OUTSIDE_EXT), - WIND(FeatureArea.WEATHER, "NAModule2", 1, "device", WEATHER_STATION, Set.of(ChannelHelperCapability.class), - ChannelGroup.SIGNAL, ChannelGroup.TSTAMP_EXT, ChannelGroup.BATTERY, - new ChannelGroup(WindChannelHelper.class, GROUP_WIND)), + WIND(FeatureArea.WEATHER, "NAModule2", 1, "device", WEATHER_STATION, + Set.of(ChannelHelperCapability.class, ParentUpdateCapability.class), ChannelGroup.SIGNAL, + ChannelGroup.TSTAMP_EXT, ChannelGroup.BATTERY, new ChannelGroup(WindChannelHelper.class, GROUP_WIND)), RAIN(FeatureArea.WEATHER, "NAModule3", 1, "device", WEATHER_STATION, - Set.of(MeasureCapability.class, ChannelHelperCapability.class), ChannelGroup.SIGNAL, - ChannelGroup.TSTAMP_EXT, ChannelGroup.MEASURE, ChannelGroup.BATTERY, + Set.of(MeasureCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), + ChannelGroup.SIGNAL, ChannelGroup.TSTAMP_EXT, ChannelGroup.MEASURE, ChannelGroup.BATTERY, new ChannelGroup(RainChannelHelper.class, MeasureClass.RAIN_QUANTITY, GROUP_RAIN)), INDOOR(FeatureArea.WEATHER, "NAModule4", 1, "device", WEATHER_STATION, - Set.of(MeasureCapability.class, ChannelHelperCapability.class), ChannelGroup.SIGNAL, - ChannelGroup.TSTAMP_EXT, ChannelGroup.MEASURE, ChannelGroup.BATTERY, ChannelGroup.HUMIDITY, - ChannelGroup.TEMP_INSIDE_EXT, ChannelGroup.AIR_QUALITY), + Set.of(MeasureCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), + ChannelGroup.SIGNAL, ChannelGroup.TSTAMP_EXT, ChannelGroup.MEASURE, ChannelGroup.BATTERY, + ChannelGroup.HUMIDITY, ChannelGroup.TEMP_INSIDE_EXT, ChannelGroup.AIR_QUALITY), - HOME_COACH(FeatureArea.AIR_CARE, "NHC", 1, "device", ACCOUNT, + HOME_COACH(FeatureArea.AIR_CARE, "NHC", 1, "weather", ACCOUNT, Set.of(DeviceCapability.class, AirCareCapability.class, MeasureCapability.class, - ChannelHelperCapability.class), + ChannelHelperCapability.class, RefreshAutoCapability.class), ChannelGroup.LOCATION, ChannelGroup.SIGNAL, ChannelGroup.NOISE, ChannelGroup.HUMIDITY, ChannelGroup.TEMP_INSIDE, ChannelGroup.MEASURE, ChannelGroup.TSTAMP_EXT, new ChannelGroup(AirQualityChannelHelper.class, GROUP_TYPE_AIR_QUALITY_EXTENDED), new ChannelGroup(PressureChannelHelper.class, MeasureClass.PRESSURE, GROUP_PRESSURE)), - PLUG(FeatureArea.ENERGY, "NAPlug", 1, "device", HOME, Set.of(ChannelHelperCapability.class), ChannelGroup.SIGNAL), + PLUG(FeatureArea.ENERGY, "NAPlug", 1, "device", HOME, + Set.of(ChannelHelperCapability.class, ParentUpdateCapability.class), ChannelGroup.SIGNAL), - VALVE(FeatureArea.ENERGY, "NRV", 1, "device", PLUG, Set.of(ChannelHelperCapability.class), ChannelGroup.SIGNAL, + VALVE(FeatureArea.ENERGY, "NRV", 1, "device", PLUG, + Set.of(ChannelHelperCapability.class, ParentUpdateCapability.class), ChannelGroup.SIGNAL, ChannelGroup.BATTERY_EXT), - THERMOSTAT(FeatureArea.ENERGY, "NATherm1", 1, "device", PLUG, Set.of(ChannelHelperCapability.class), - ChannelGroup.SIGNAL, ChannelGroup.BATTERY_EXT, - new ChannelGroup(Therm1ChannelHelper.class, GROUP_TYPE_TH_PROPERTIES)), + THERMOSTAT(FeatureArea.ENERGY, "NATherm1", 1, "device", PLUG, + Set.of(ChannelHelperCapability.class, ParentUpdateCapability.class), ChannelGroup.SIGNAL, + ChannelGroup.BATTERY_EXT, new ChannelGroup(Therm1ChannelHelper.class, GROUP_TYPE_TH_PROPERTIES)), - ROOM(FeatureArea.ENERGY, "NARoom", 1, "virtual", HOME, Set.of(RoomCapability.class, ChannelHelperCapability.class), + ROOM(FeatureArea.ENERGY, "NARoom", 1, "virtual", HOME, + Set.of(RoomCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), new ChannelGroup(RoomChannelHelper.class, GROUP_TYPE_ROOM_PROPERTIES, GROUP_TYPE_ROOM_TEMPERATURE), new ChannelGroup(SetpointChannelHelper.class, GROUP_SETPOINT)), SMOKE_DETECTOR(FeatureArea.SECURITY, "NSD", 1, "device", HOME, - Set.of(AlarmEventCapability.class, ChannelHelperCapability.class), ChannelGroup.SIGNAL, - ChannelGroup.TIMESTAMP, ChannelGroup.ALARM_LAST_EVENT), + Set.of(AlarmEventCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), + ChannelGroup.SIGNAL, ChannelGroup.TIMESTAMP, ChannelGroup.ALARM_LAST_EVENT), CO_DETECTOR(FeatureArea.SECURITY, "NCO", 1, "device", HOME, - Set.of(AlarmEventCapability.class, ChannelHelperCapability.class), ChannelGroup.SIGNAL, - ChannelGroup.TIMESTAMP, ChannelGroup.ALARM_LAST_EVENT); + Set.of(AlarmEventCapability.class, ChannelHelperCapability.class, ParentUpdateCapability.class), + ChannelGroup.SIGNAL, ChannelGroup.TIMESTAMP, ChannelGroup.ALARM_LAST_EVENT); public static final EnumSet AS_SET = EnumSet.allOf(ModuleType.class); diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/HomeConfiguration.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/HomeConfiguration.java index efbb3b398f8..3a44b50347c 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/HomeConfiguration.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/HomeConfiguration.java @@ -31,6 +31,12 @@ public class HomeConfiguration extends NAThingConfiguration { return getIdForArea(energyId.isBlank() ? FeatureArea.SECURITY : FeatureArea.ENERGY); } + @Override + public int getRefreshInterval() { + int local = refreshInterval; + return local == -1 ? 180 : local; + } + public String getIdForArea(FeatureArea feature) { return FeatureArea.ENERGY.equals(feature) ? energyId.isBlank() ? id : energyId : FeatureArea.SECURITY.equals(feature) ? securityId.isBlank() ? id : securityId : id; diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/NAThingConfiguration.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/NAThingConfiguration.java index 2fc3f58d9f0..95c23e930ee 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/NAThingConfiguration.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/config/NAThingConfiguration.java @@ -25,7 +25,12 @@ public class NAThingConfiguration { public static final String ID = "id"; protected String id = ""; - public int refreshInterval = -1; + protected int refreshInterval = -1; + + public int getRefreshInterval() { + int local = refreshInterval; + return local == -1 ? 600 : local; + } public String getId() { return id; diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/CommonInterface.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/CommonInterface.java index da571ce7a85..51beb8de5cc 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/CommonInterface.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/CommonInterface.java @@ -12,10 +12,13 @@ */ package org.openhab.binding.netatmo.internal.handler; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.eclipse.jdt.annotation.NonNullByDefault; @@ -28,8 +31,6 @@ import org.openhab.binding.netatmo.internal.config.NAThingConfiguration; import org.openhab.binding.netatmo.internal.handler.capability.Capability; import org.openhab.binding.netatmo.internal.handler.capability.CapabilityMap; import org.openhab.binding.netatmo.internal.handler.capability.HomeCapability; -import org.openhab.binding.netatmo.internal.handler.capability.ParentUpdateCapability; -import org.openhab.binding.netatmo.internal.handler.capability.RefreshCapability; import org.openhab.binding.netatmo.internal.handler.capability.RestCapability; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Channel; @@ -87,6 +88,10 @@ public interface CommonInterface { : null; } + default Optional> schedule(Runnable arg0, Duration delay) { + return Optional.of(getScheduler().schedule(arg0, delay.getSeconds(), TimeUnit.SECONDS)); + } + default @Nullable ApiBridgeHandler getAccountHandler() { Bridge bridge = getBridge(); BridgeHandler bridgeHandler = null; @@ -221,15 +226,14 @@ public interface CommonInterface { setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED, null); } else if (!ThingStatus.ONLINE.equals(bridge.getStatus())) { setThingStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, null); - getCapabilities().remove(RefreshCapability.class); - getCapabilities().remove(ParentUpdateCapability.class); + getCapabilities().getParentUpdate().ifPresent(Capability::dispose); + getCapabilities().getRefresh().ifPresent(Capability::dispose); } else { setThingStatus(ThingStatus.UNKNOWN, ThingStatusDetail.NONE, null); - if (ModuleType.ACCOUNT.equals(getModuleType().getBridge())) { - NAThingConfiguration config = getThing().getConfiguration().as(NAThingConfiguration.class); - getCapabilities().put(new RefreshCapability(this, config.refreshInterval)); - } - getCapabilities().put(new ParentUpdateCapability(this)); + getCapabilities().getParentUpdate().ifPresentOrElse(Capability::initialize, () -> { + int interval = getThingConfigAs(NAThingConfiguration.class).getRefreshInterval(); + getCapabilities().getRefresh().ifPresent(cap -> cap.setInterval(Duration.ofSeconds(interval))); + }); } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CacheCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CacheCapability.java index b2ecd79482f..88561ccd403 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CacheCapability.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CacheCapability.java @@ -46,7 +46,7 @@ public abstract class CacheCapability extends RestCapabil protected synchronized List updateReadings(T api) { Instant now = Instant.now(); - if (requestTS.plus(validity).isBefore(now)) { + if (!stillValid(now)) { logger.debug("{} requesting fresh data for {}", getClass().getSimpleName(), thingUID); List result = getFreshData(api); if (!result.isEmpty()) { @@ -58,5 +58,9 @@ public abstract class CacheCapability extends RestCapabil return lastResult; } + protected boolean stillValid(Instant ts) { + return requestTS.plus(validity).isAfter(ts); + } + protected abstract List getFreshData(T api); } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/Capability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/Capability.java index 3ca3a89b0fa..749a6817e87 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/Capability.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/Capability.java @@ -153,7 +153,7 @@ public class Capability { public void expireData() { CommonInterface bridgeHandler = handler.getBridgeHandler(); - if (bridgeHandler != null && !handler.getCapabilities().containsKey(RefreshCapability.class)) { + if (bridgeHandler != null && handler.getCapabilities().getRefresh().isEmpty()) { bridgeHandler.expireData(); } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CapabilityMap.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CapabilityMap.java index 8b7517cc470..301edc53819 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CapabilityMap.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/CapabilityMap.java @@ -49,4 +49,13 @@ public class CapabilityMap extends ConcurrentHashMap, Capability> { cap.dispose(); } } + + public Optional getRefresh() { + return values().stream().filter(RefreshCapability.class::isInstance).map(RefreshCapability.class::cast) + .findFirst(); + } + + public Optional getParentUpdate() { + return values().stream().filter(ParentUpdateCapability.class::isInstance).findFirst(); + } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/ParentUpdateCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/ParentUpdateCapability.java index 9e36f1aba12..28a1b6ea716 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/ParentUpdateCapability.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/ParentUpdateCapability.java @@ -12,9 +12,9 @@ */ package org.openhab.binding.netatmo.internal.handler.capability; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.netatmo.internal.handler.CommonInterface; @@ -29,7 +29,7 @@ import org.slf4j.LoggerFactory; */ @NonNullByDefault public class ParentUpdateCapability extends Capability { - private static final int DEFAULT_DELAY_S = 2; + private static final Duration DEFAULT_DELAY = Duration.ofSeconds(2); private final Logger logger = LoggerFactory.getLogger(ParentUpdateCapability.class); private Optional> job = Optional.empty(); @@ -40,13 +40,13 @@ public class ParentUpdateCapability extends Capability { @Override public void initialize() { - job = Optional.of(handler.getScheduler().schedule(() -> { - logger.debug("Requesting parents data update for Thing {}", handler.getId()); + job = handler.schedule(() -> { + logger.debug("Requesting parents data update for Thing '{}'", thingUID); CommonInterface bridgeHandler = handler.getBridgeHandler(); if (bridgeHandler != null) { bridgeHandler.expireData(); } - }, DEFAULT_DELAY_S, TimeUnit.SECONDS)); + }, DEFAULT_DELAY); } @Override diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/RefreshAutoCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/RefreshAutoCapability.java new file mode 100644 index 00000000000..d230df57f20 --- /dev/null +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/RefreshAutoCapability.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2010-2024 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.netatmo.internal.handler.capability; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.netatmo.internal.api.dto.NAObject; +import org.openhab.binding.netatmo.internal.api.dto.NAThing; +import org.openhab.binding.netatmo.internal.handler.CommonInterface; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link RefreshAutoCapability} implements probing and auto-adjusting refresh strategy + * + * @author Gaël L'hopital - Initial contribution + * + */ +@NonNullByDefault +public class RefreshAutoCapability extends RefreshCapability { + private static final Duration DEFAULT_DELAY = Duration.ofSeconds(15); + + private final Logger logger = LoggerFactory.getLogger(RefreshAutoCapability.class); + + private Instant dataTimeStamp = Instant.MIN; + + public RefreshAutoCapability(CommonInterface handler) { + super(handler); + } + + @Override + public void expireData() { + dataTimeStamp = Instant.MIN; + super.expireData(); + } + + @Override + protected Duration calcDelay() { + if (Instant.MIN.equals(dataTimeStamp)) { + return PROBING_INTERVAL; + } + + Duration dataAge = Duration.between(dataTimeStamp, Instant.now()); + + Duration delay = dataValidity.minus(dataAge); + if (delay.isNegative() || delay.isZero()) { + logger.debug("{} did not update data in expected time, return to probing", thingUID); + dataTimeStamp = Instant.MIN; + return PROBING_INTERVAL; + } + + return delay.plus(DEFAULT_DELAY); + } + + @Override + protected void updateNAThing(NAThing newData) { + super.updateNAThing(newData); + dataTimeStamp = newData.getLastSeen().map(ZonedDateTime::toInstant).orElse(Instant.MIN); + } + + @Override + protected void afterNewData(@Nullable NAObject newData) { + properties.put("probing", Boolean.valueOf(Instant.MIN.equals(dataTimeStamp)).toString()); + super.afterNewData(newData); + } +} diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/RefreshCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/RefreshCapability.java index 26aba9e1c71..59a1348afce 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/RefreshCapability.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/RefreshCapability.java @@ -12,114 +12,107 @@ */ package org.openhab.binding.netatmo.internal.handler.capability; -import static java.time.temporal.ChronoUnit.*; - import java.time.Duration; import java.time.Instant; -import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.netatmo.internal.api.dto.NAThing; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.netatmo.internal.api.dto.NAObject; import org.openhab.binding.netatmo.internal.handler.CommonInterface; import org.openhab.core.thing.ThingStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * {@link RefreshCapability} is the class used to embed the refreshing needs calculation for devices + * {@link RefreshCapability} is the base class used to define refreshing policies + * It implements of a fixed refresh rate strategy. * * @author Gaël L'hopital - Initial contribution * */ @NonNullByDefault public class RefreshCapability extends Capability { - private static final Duration DEFAULT_DELAY = Duration.of(20, SECONDS); - private static final Duration PROBING_INTERVAL = Duration.of(120, SECONDS); - private static final Duration OFFLINE_INTERVAL = Duration.of(15, MINUTES); + protected static final Duration ASAP = Duration.ofSeconds(2); + protected static final Duration OFFLINE_DELAY = Duration.ofMinutes(15); + protected static final Duration PROBING_INTERVAL = Duration.ofMinutes(2); private final Logger logger = LoggerFactory.getLogger(RefreshCapability.class); - private Duration dataValidity; - private Instant dataTimeStamp = Instant.now(); - private Instant dataTimeStamp0 = Instant.MIN; + protected Duration dataValidity = PROBING_INTERVAL; private Optional> refreshJob = Optional.empty(); - private boolean refreshConfigured; + private boolean expiring = false; - public RefreshCapability(CommonInterface handler, int refreshInterval) { + public RefreshCapability(CommonInterface handler) { super(handler); - this.dataValidity = Duration.ofSeconds(Math.max(0, refreshInterval)); } - @Override - public void initialize() { - this.refreshConfigured = !probing(); - freeJobAndReschedule(2); + public void setInterval(Duration dataValidity) { + if (dataValidity.isNegative() || dataValidity.isZero()) { + throw new IllegalArgumentException("refreshInterval must be positive"); + } + this.dataValidity = dataValidity; + expireData(); } @Override public void dispose() { - freeJobAndReschedule(0); + stopJob(); super.dispose(); } @Override public void expireData() { - dataTimeStamp = Instant.now().minus(dataValidity); - freeJobAndReschedule(1); - } - - private Duration dataAge() { - return Duration.between(dataTimeStamp, Instant.now()); - } - - private boolean probing() { - return dataValidity.getSeconds() <= 0; - } - - private void proceedWithUpdate() { - handler.proceedWithUpdate(); - long delay; - if (!ThingStatus.ONLINE.equals(handler.getThing().getStatus())) { - logger.debug("{} is not ONLINE, special refresh interval is used", thingUID); - delay = OFFLINE_INTERVAL.toSeconds(); - if (probing()) { - dataTimeStamp0 = Instant.MIN; - } - } else { - delay = refreshConfigured ? dataValidity.getSeconds() - : (probing() ? PROBING_INTERVAL : dataValidity.minus(dataAge()).plus(DEFAULT_DELAY)).toSeconds(); + if (!expiring) { + expiring = true; + rescheduleJob(ASAP); } - delay = delay < 2 ? PROBING_INTERVAL.toSeconds() : delay; - logger.debug("{} refreshed, next one in {}s", thingUID, delay); - freeJobAndReschedule(delay); } @Override - protected void updateNAThing(NAThing newData) { - super.updateNAThing(newData); - newData.getLastSeen().map(ZonedDateTime::toInstant).ifPresent(tsInstant -> { - if (probing()) { - if (Instant.MIN.equals(dataTimeStamp0)) { - dataTimeStamp0 = tsInstant; - logger.debug("First data timestamp of {} is {}", thingUID, dataTimeStamp0); - } else if (tsInstant.isAfter(dataTimeStamp0)) { - dataValidity = Duration.between(dataTimeStamp0, tsInstant); - refreshConfigured = true; - logger.debug("Data validity period of {} identified to be {}", thingUID, dataValidity); - } else { - logger.debug("Data validity period of {} not yet found, data timestamp unchanged", thingUID); - } - } - dataTimeStamp = tsInstant; - }); + protected void afterNewData(@Nullable NAObject newData) { + expiring = false; + super.afterNewData(newData); } - private void freeJobAndReschedule(long delay) { + protected Duration calcDelay() { + return dataValidity; + } + + private void proceedWithUpdate() { + Duration delay; + handler.proceedWithUpdate(); + if (!ThingStatus.ONLINE.equals(handler.getThing().getStatus())) { + delay = OFFLINE_DELAY; + logger.debug("Thing '{}' is not ONLINE, using special refresh interval", thingUID); + } else { + delay = calcDelay(); + } + rescheduleJob(delay); + } + + private void rescheduleJob(Duration delay) { + if (refreshJob.isPresent()) { + ScheduledFuture job = refreshJob.get(); + Instant now = Instant.now(); + Instant expectedExecution = now.plus(delay); + Instant scheduledExecution = now.plusMillis(job.getDelay(TimeUnit.MILLISECONDS)); + if (Math.abs(ChronoUnit.SECONDS.between(expectedExecution, scheduledExecution)) <= 3) { + logger.debug("'{}' refresh as already pending roughly as the same time, will not reschedule", thingUID); + return; + } else { + stopJob(); + } + } + logger.debug("'{}' next refresh in {}", thingUID, delay); + refreshJob = handler.schedule(this::proceedWithUpdate, delay); + } + + private void stopJob() { refreshJob.ifPresent(job -> job.cancel(true)); - refreshJob = Optional.ofNullable(delay == 0 ? null - : handler.getScheduler().schedule(() -> proceedWithUpdate(), delay, TimeUnit.SECONDS)); + refreshJob = Optional.empty(); } } diff --git a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/WeatherCapability.java b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/WeatherCapability.java index 4789af45877..72a5bdda6d0 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/WeatherCapability.java +++ b/bundles/org.openhab.binding.netatmo/src/main/java/org/openhab/binding/netatmo/internal/handler/capability/WeatherCapability.java @@ -34,7 +34,7 @@ public class WeatherCapability extends CacheCapability { private final Logger logger = LoggerFactory.getLogger(WeatherCapability.class); public WeatherCapability(CommonInterface handler) { - super(handler, Duration.ofSeconds(2), WeatherApi.class); + super(handler, Duration.ofSeconds(10), WeatherApi.class); } @Override diff --git a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/config/config.xml index 40fb6f0f868..e7866d83ae6 100644 --- a/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/config/config.xml +++ b/bundles/org.openhab.binding.netatmo/src/main/resources/OH-INF/config/config.xml @@ -103,6 +103,19 @@ + + + + @text/config.equipmentId.description + + + + @text/config.refreshInterval.description + 600 + true + + + @@ -140,7 +153,7 @@ true - + @text/config.refreshInterval.description 180 diff --git a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/api/data/ModuleTypeTest.java b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/api/data/ModuleTypeTest.java index 3698a461527..b67bdb9d3ed 100644 --- a/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/api/data/ModuleTypeTest.java +++ b/bundles/org.openhab.binding.netatmo/src/test/java/org/openhab/binding/netatmo/internal/api/data/ModuleTypeTest.java @@ -30,6 +30,10 @@ public class ModuleTypeTest { // This did not exist prior to PR #16492 return URI.create(BINDING_ID + ":camera"); } + if (mt == ModuleType.WEATHER_STATION || mt == ModuleType.HOME_COACH) { + // This did not exist prior to PR #16492 + return URI.create(BINDING_ID + ":weather"); + } // This was previous method for calculating configuration URI return URI.create(BINDING_ID + ":" + (mt == ModuleType.ACCOUNT ? "api_bridge"