mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[sagercaster] Simplify Thing configuration (#19574)
* Simplify Thing configuration Adapt to Java 21 and OH5 Signed-off-by: gael@lhopital.org <gael@lhopital.org>
This commit is contained in:
@@ -6,7 +6,7 @@ The Sager Weathercaster is a scientific instrument for accurate prediction of th
|
||||
|
||||
- To operate, this binding will need to use channel values provided by other means (e.g. Weather Binding, Netatmo, a 1-Wire personal weather station...)
|
||||
|
||||
- This binding buffers readings for some hours before producing weather forecasts (wind direction and sea level pressure). SagerWeatherCaster needs an observation period of minimum 6 hours.
|
||||
- This binding buffers readings up to 6 hours before producing reliable weather forecasts (wind direction and sea level pressure).
|
||||
|
||||
For these reasons, this binding is not a binding in the usual sense.
|
||||
|
||||
@@ -22,10 +22,9 @@ The binding itself does not require any configuration.
|
||||
|
||||
### SagerCaster
|
||||
|
||||
| Name | Type | Description |
|
||||
| ------------------ | -------- | -------------------------------------------------------------------- |
|
||||
| location (*) | Location | Latitude and longitude of the desired weather forecast. |
|
||||
| observation-period | int | Minimum delay (in hours) before producing forecasts. Defaulted to 6. |
|
||||
| Name | Type | Description |
|
||||
| ------------ | -------- | ------------------------------------------------------ |
|
||||
| location (*) | Location | Latitude and longitude of the desired weather forecast |
|
||||
|
||||
(*) Only latitude is used by the algorithm.
|
||||
|
||||
@@ -33,26 +32,27 @@ The binding itself does not require any configuration.
|
||||
|
||||
The binding will use some input channels, that can be configured directly with profiles (sample below).
|
||||
|
||||
| Name | Group | Type | Description |
|
||||
| ------------------- | ------ | -------------------- | --------------------------------------------------------------- |
|
||||
| is-raining (*) | input | Switch | On if it is raining, else Off. |
|
||||
| rain-qtty (*) | input | Number | Any value that give indication of a current rain volume |
|
||||
| or | input | Number:Speed | Any value that give indication of a current rain volume eg mm/h |
|
||||
| or | input | Number:Length | Any value that give indication of a current rain volume eg mm |
|
||||
| cloudiness | input | Number:Dimensionless | Cloud cover percentage |
|
||||
| wind-speed-beaufort | input | Number | Wind speed expressed using the Beaufort scale |
|
||||
| pressure | input | Number:Pressure | Sea level pressure |
|
||||
| wind-angle | input | Number:Angle | Wind direction |
|
||||
| temperature | input | Number:Temperature | Outside temperature |
|
||||
| timestamp | output | DateTime | Timestamp of the last forecast update |
|
||||
| forecast | output | String | Description of the weather forecast |
|
||||
| velocity | output | String | Description of the expected wind evolution |
|
||||
| velocity-beaufort | output | Number | Expected wind evolution using the Beaufort scale |
|
||||
| wind-from | output | String | Expected wind orientation |
|
||||
| wind-to | output | String | Evolution of the expected wind orientation |
|
||||
| wind-evolution | output | String | Wind orientation evolution over observation period |
|
||||
| pressure-trend | output | String | Pressure evolution over observation period |
|
||||
| temperature-trend | output | String | Temperature evolution over observation period |
|
||||
| Name | Group | Type | Description |
|
||||
| ------------------- | ------ | -------------------- | --------------------------------------------------------------------- |
|
||||
| is-raining (*) | input | Switch | On if it is raining, else Off. |
|
||||
| rain-qtty (*) | input | Number | Any value that give indication of a current rain volume |
|
||||
| or | input | Number:Speed | Any value that give indication of a current rain volume eg mm/h |
|
||||
| or | input | Number:Length | Any value that give indication of a current rain volume eg mm |
|
||||
| cloudiness | input | Number:Dimensionless | Cloud cover percentage |
|
||||
| wind-speed-beaufort | input | Number | Wind speed expressed using the Beaufort scale |
|
||||
| pressure | input | Number:Pressure | Sea level pressure |
|
||||
| wind-angle | input | Number:Angle | Wind direction |
|
||||
| temperature | input | Number:Temperature | Outside temperature |
|
||||
| timestamp | output | DateTime | Timestamp of the last forecast update |
|
||||
| forecast | output | String | Description of the weather forecast |
|
||||
| velocity | output | String | Description of the expected wind evolution |
|
||||
| velocity-beaufort | output | Number | Expected wind evolution using the Beaufort scale |
|
||||
| wind-from | output | String | Expected wind orientation |
|
||||
| wind-to | output | String | Evolution of the expected wind orientation |
|
||||
| wind-evolution | output | String | Wind orientation evolution over observation period |
|
||||
| pressure-trend | output | String | Pressure evolution over observation period |
|
||||
| temperature-trend | output | String | Temperature evolution over observation period |
|
||||
| reliability | output | Number:Dimensionless | Algorithm efficiency toward buffered data, grows during first 6 hours |
|
||||
|
||||
(*) You may use either is-raining, either rain-qtty depending upon the data available in your system.
|
||||
|
||||
|
||||
+2
-1
@@ -31,7 +31,7 @@ public class SagerCasterBindingConstants {
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_SAGERCASTER = new ThingTypeUID(BINDING_ID, "sagercaster");
|
||||
|
||||
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SAGERCASTER);
|
||||
// Configuration elements
|
||||
public static final String CONFIG_LOCATION = "location";
|
||||
public static final String CONFIG_PERIOD = "observation-period";
|
||||
@@ -49,6 +49,7 @@ public class SagerCasterBindingConstants {
|
||||
public static final String CHANNEL_PRESSURETREND = "pressure-trend";
|
||||
public static final String CHANNEL_TEMPERATURETREND = "temperature-trend";
|
||||
public static final String CHANNEL_TIMESTAMP = "timestamp";
|
||||
public static final String CHANNEL_RELIABILITY = "reliability";
|
||||
|
||||
// Input channel ids
|
||||
public static final String CHANNEL_CLOUDINESS = "cloudiness";
|
||||
|
||||
+3
-6
@@ -12,9 +12,7 @@
|
||||
*/
|
||||
package org.openhab.binding.sagercaster.internal;
|
||||
|
||||
import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstants.THING_TYPE_SAGERCASTER;
|
||||
|
||||
import java.util.Set;
|
||||
import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstants.*;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
@@ -38,13 +36,12 @@ import org.osgi.service.component.annotations.Reference;
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.sagercaster")
|
||||
@NonNullByDefault
|
||||
public class SagerCasterHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_SAGERCASTER);
|
||||
private final WindDirectionStateDescriptionProvider stateDescriptionProvider;
|
||||
private final SagerWeatherCaster sagerWeatherCaster;
|
||||
|
||||
@Activate
|
||||
public SagerCasterHandlerFactory(@Reference SagerWeatherCaster sagerWeatherCaster,
|
||||
@Reference WindDirectionStateDescriptionProvider provider) {
|
||||
public SagerCasterHandlerFactory(final @Reference SagerWeatherCaster sagerWeatherCaster,
|
||||
final @Reference WindDirectionStateDescriptionProvider provider) {
|
||||
this.stateDescriptionProvider = provider;
|
||||
this.sagerWeatherCaster = sagerWeatherCaster;
|
||||
}
|
||||
|
||||
+8
-16
@@ -13,37 +13,29 @@
|
||||
package org.openhab.binding.sagercaster.internal.caster;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class holds the result of the SagerCaster algorithm
|
||||
* This record holds the result of the SagerCaster algorithm
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SagerPrediction {
|
||||
private final String prediction;
|
||||
|
||||
public SagerPrediction(String sagerCode) {
|
||||
this.prediction = sagerCode;
|
||||
}
|
||||
|
||||
public String getSagerCode() {
|
||||
return prediction;
|
||||
}
|
||||
public record SagerPrediction(String sagerCode) {
|
||||
|
||||
public String getForecast() {
|
||||
return Character.toString(prediction.charAt(0));
|
||||
return Character.toString(sagerCode.charAt(0));
|
||||
}
|
||||
|
||||
public String getWindVelocity() {
|
||||
return Character.toString(prediction.charAt(1));
|
||||
return Character.toString(sagerCode.charAt(1));
|
||||
}
|
||||
|
||||
public String getWindDirection() {
|
||||
return Character.toString(prediction.charAt(2));
|
||||
return Character.toString(sagerCode.charAt(2));
|
||||
}
|
||||
|
||||
public String getWindDirection2() {
|
||||
return prediction.length() > 3 ? Character.toString(prediction.charAt(3)) : SagerWeatherCaster.UNDEF;
|
||||
public @Nullable String getWindDirection2() {
|
||||
return sagerCode.length() > 3 ? Character.toString(sagerCode.charAt(3)) : null;
|
||||
}
|
||||
}
|
||||
|
||||
+60
-96
@@ -77,7 +77,6 @@ import org.slf4j.LoggerFactory;
|
||||
@Component(service = SagerWeatherCaster.class, scope = ServiceScope.SINGLETON)
|
||||
@NonNullByDefault
|
||||
public class SagerWeatherCaster {
|
||||
public static final String UNDEF = "-";
|
||||
// Northern Polar Zone & Northern Tropical Zone
|
||||
private static final String[] NPZDIRECTIONS = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" };
|
||||
// Northern Temperate Zone
|
||||
@@ -101,7 +100,7 @@ public class SagerWeatherCaster {
|
||||
// barometer for a period of about 6 hours prior to the forecast.
|
||||
private int nubes = -1;
|
||||
private int currentBeaufort = -1;
|
||||
private double cloudLevel = -1;
|
||||
private Integer cloudLevel = -1;
|
||||
private boolean raining = false;
|
||||
|
||||
@Activate
|
||||
@@ -169,15 +168,12 @@ public class SagerWeatherCaster {
|
||||
private void sagerNubesUpdate() {
|
||||
int result;
|
||||
if (!raining) {
|
||||
if (cloudLevel > 80) {
|
||||
result = 4; // overcast
|
||||
} else if (cloudLevel > 50) {
|
||||
result = 3; // mostly overcast
|
||||
} else if (cloudLevel > 20) {
|
||||
result = 2; // partly cloudy
|
||||
} else {
|
||||
result = 1; // clear
|
||||
}
|
||||
result = switch (cloudLevel) {
|
||||
case Integer i when i > 80 -> 4; // overcast
|
||||
case Integer i when i > 50 -> 3; // mostly overcast
|
||||
case Integer i when i > 20 -> 2; // partly cloudy
|
||||
default -> 1; // clear
|
||||
};
|
||||
} else {
|
||||
result = 5; // raining
|
||||
}
|
||||
@@ -187,38 +183,29 @@ public class SagerWeatherCaster {
|
||||
}
|
||||
}
|
||||
|
||||
private static int sagerPressureLevel(double current) {
|
||||
if (current > 1029.46) {
|
||||
return 1;
|
||||
} else if (current > 1019.3) {
|
||||
return 2;
|
||||
} else if (current > 1012.53) {
|
||||
return 3;
|
||||
} else if (current > 1005.76) {
|
||||
return 4;
|
||||
} else if (current > 999) {
|
||||
return 5;
|
||||
} else if (current > 988.8) {
|
||||
return 6;
|
||||
} else if (current > 975.28) {
|
||||
return 7;
|
||||
}
|
||||
return 8;
|
||||
private static int sagerPressureLevel(Double current) {
|
||||
return switch (current) {
|
||||
case Double c when c > 1029.46 -> 1;
|
||||
case Double c when c > 1019.3 -> 2;
|
||||
case Double c when c > 1012.53 -> 3;
|
||||
case Double c when c > 1005.76 -> 4;
|
||||
case Double c when c > 999 -> 5;
|
||||
case Double c when c > 988.8 -> 6;
|
||||
case Double c when c > 975.28 -> 7;
|
||||
default -> 8;
|
||||
};
|
||||
}
|
||||
|
||||
private static int sagerPressureTrend(double current, double historic) {
|
||||
double evol = current - historic;
|
||||
Double evol = current - historic;
|
||||
|
||||
if (evol > 1.4) {
|
||||
return 1; // Rising Rapidly
|
||||
} else if (evol > 0.68) {
|
||||
return 2; // Rising Slowly
|
||||
} else if (evol > -0.68) {
|
||||
return 3; // Normal
|
||||
} else if (evol > -1.4) {
|
||||
return 4; // Decreasing Slowly
|
||||
}
|
||||
return 5; // Decreasing Rapidly
|
||||
return switch (evol) {
|
||||
case Double e when e > 1.4 -> 1; // Rising Rapidly
|
||||
case Double e when e > 0.68 -> 2; // Rising Slowly
|
||||
case Double e when e > -0.68 -> 3; // Normal
|
||||
case Double e when e > -1.4 -> 4; // Decreasing Slowly
|
||||
default -> 5; // Decreasing Rapidly
|
||||
};
|
||||
}
|
||||
|
||||
private static int sagerWindTrend(double historic, double position) {
|
||||
@@ -233,14 +220,14 @@ public class SagerWeatherCaster {
|
||||
}
|
||||
|
||||
private String getCompass() {
|
||||
double step = 360.0 / NTZDIRECTIONS.length;
|
||||
double step = 360.0 / usedDirections.length;
|
||||
double b = Math.floor((currentBearing + (step / 2.0)) / step);
|
||||
return NTZDIRECTIONS[(int) (b % NTZDIRECTIONS.length)];
|
||||
return usedDirections[(int) (b % usedDirections.length)];
|
||||
}
|
||||
|
||||
private void updatePrediction() {
|
||||
int zWind = Arrays.asList(usedDirections).indexOf(getCompass());
|
||||
String d1 = UNDEF;
|
||||
String d1 = null;
|
||||
switch (zWind) {
|
||||
case 0:
|
||||
if (windEvolution == 3) {
|
||||
@@ -319,75 +306,52 @@ public class SagerWeatherCaster {
|
||||
d1 = "Z";
|
||||
}
|
||||
}
|
||||
String forecast = forecaster.getProperty(d1 + sagerPressure + pressureEvolution + nubes);
|
||||
String forecast = d1 == null ? null : forecaster.getProperty(d1 + sagerPressure + pressureEvolution + nubes);
|
||||
prevision = forecast != null ? new SagerPrediction(forecast) : null;
|
||||
}
|
||||
|
||||
public String getForecast() {
|
||||
return Objects.requireNonNullElse(this.prevision.getForecast(), UNDEF);
|
||||
public @Nullable String getForecast() {
|
||||
return prevision instanceof SagerPrediction p ? p.getForecast() : null;
|
||||
}
|
||||
|
||||
public String getWindVelocity() {
|
||||
SagerPrediction prevision = this.prevision;
|
||||
return prevision != null ? prevision.getWindVelocity() : UNDEF;
|
||||
public @Nullable String getWindVelocity() {
|
||||
return prevision instanceof SagerPrediction p ? p.getWindVelocity() : null;
|
||||
}
|
||||
|
||||
public String getWindDirection() {
|
||||
SagerPrediction prevision = this.prevision;
|
||||
return prevision != null ? prevision.getWindDirection() : UNDEF;
|
||||
public @Nullable String getWindDirection() {
|
||||
return prevision instanceof SagerPrediction p ? p.getWindDirection() : null;
|
||||
}
|
||||
|
||||
public String getWindDirection2() {
|
||||
SagerPrediction prevision = this.prevision;
|
||||
return prevision != null ? prevision.getWindDirection2() : UNDEF;
|
||||
public @Nullable String getWindDirection2() {
|
||||
return prevision instanceof SagerPrediction p ? p.getWindDirection2() : null;
|
||||
}
|
||||
|
||||
public String getSagerCode() {
|
||||
SagerPrediction prevision = this.prevision;
|
||||
return prevision != null ? prevision.getSagerCode() : UNDEF;
|
||||
public @Nullable String getSagerCode() {
|
||||
return prevision instanceof SagerPrediction p ? p.sagerCode() : null;
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
if (latitude >= 66.6) {
|
||||
usedDirections = NPZDIRECTIONS;
|
||||
} else if (latitude >= 23.5) {
|
||||
usedDirections = NTZDIRECTIONS;
|
||||
} else if (latitude >= 0) {
|
||||
usedDirections = NPZDIRECTIONS;
|
||||
} else if (latitude > -23.5) {
|
||||
usedDirections = SPZDIRECTIONS;
|
||||
} else if (latitude > -66.6) {
|
||||
usedDirections = STZDIRECTIONS;
|
||||
} else {
|
||||
usedDirections = SPZDIRECTIONS;
|
||||
}
|
||||
public void setLatitude(Double latitude) {
|
||||
usedDirections = switch (latitude) {
|
||||
case Double d when d >= 66.6 -> NPZDIRECTIONS;
|
||||
case Double d when d >= 23.5 -> NTZDIRECTIONS;
|
||||
case Double d when d >= 0 -> NPZDIRECTIONS;
|
||||
case Double d when d > -23.5 -> SPZDIRECTIONS;
|
||||
case Double d when d > -66.6 -> STZDIRECTIONS;
|
||||
default -> SPZDIRECTIONS;
|
||||
};
|
||||
}
|
||||
|
||||
public int getPredictedBeaufort() {
|
||||
int result = currentBeaufort;
|
||||
switch (getWindVelocity()) {
|
||||
case "N":
|
||||
result += 1;
|
||||
break;
|
||||
case "F":
|
||||
result = 4;
|
||||
break;
|
||||
case "S":
|
||||
result = 6;
|
||||
break;
|
||||
case "G":
|
||||
result = 8;
|
||||
break;
|
||||
case "W":
|
||||
result = 10;
|
||||
break;
|
||||
case "H":
|
||||
result = 12;
|
||||
break;
|
||||
case "D":
|
||||
result -= 1;
|
||||
break;
|
||||
}
|
||||
return result;
|
||||
return switch (getWindVelocity()) {
|
||||
case "N" -> currentBeaufort + 1;
|
||||
case "F" -> 4;
|
||||
case "S" -> 6;
|
||||
case "G" -> 8;
|
||||
case "W" -> 10;
|
||||
case "H" -> 12;
|
||||
case "D" -> currentBeaufort - 1;
|
||||
case null -> currentBeaufort;
|
||||
default -> currentBeaufort;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+8
-23
@@ -14,9 +14,6 @@ package org.openhab.binding.sagercaster.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstants.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -60,39 +57,28 @@ public class SagerCasterDiscoveryService extends AbstractDiscoveryService {
|
||||
@Activate
|
||||
public SagerCasterDiscoveryService(final @Reference LocaleProvider localeProvider,
|
||||
final @Reference TranslationProvider i18nProvider, final @Reference LocationProvider locationProvider) {
|
||||
super(Set.of(THING_TYPE_SAGERCASTER), DISCOVER_TIMEOUT_SECONDS, true);
|
||||
super(SUPPORTED_THING_TYPES_UIDS, DISCOVER_TIMEOUT_SECONDS, true);
|
||||
this.locationProvider = locationProvider;
|
||||
this.localeProvider = localeProvider;
|
||||
this.i18nProvider = i18nProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate(@Nullable Map<String, Object> configProperties) {
|
||||
super.activate(configProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void modified(@Nullable Map<String, Object> configProperties) {
|
||||
super.modified(configProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting Sager Weathercaster discovery scan");
|
||||
PointType location = locationProvider.getLocation();
|
||||
if (location == null) {
|
||||
if (locationProvider.getLocation() instanceof PointType location) {
|
||||
createResults(location);
|
||||
} else {
|
||||
logger.debug("LocationProvider.getLocation() is not set -> Will not provide any discovery results");
|
||||
return;
|
||||
}
|
||||
createResults(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
if (discoveryJob == null) {
|
||||
discoveryJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
PointType currentLocation = locationProvider.getLocation();
|
||||
if (currentLocation != null && !Objects.equals(currentLocation, previousLocation)) {
|
||||
if (locationProvider.getLocation() instanceof PointType currentLocation
|
||||
&& !currentLocation.equals(previousLocation)) {
|
||||
logger.debug("Location has been changed from {} to {}: Creating new discovery results",
|
||||
previousLocation, currentLocation);
|
||||
createResults(currentLocation);
|
||||
@@ -106,9 +92,8 @@ public class SagerCasterDiscoveryService extends AbstractDiscoveryService {
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
ScheduledFuture<?> localJob = discoveryJob;
|
||||
logger.debug("Stopping Sager Weathercaster background discovery");
|
||||
if (localJob != null && !localJob.isCancelled()) {
|
||||
if (discoveryJob instanceof ScheduledFuture localJob && !localJob.isCancelled()) {
|
||||
if (localJob.cancel(true)) {
|
||||
discoveryJob = null;
|
||||
logger.debug("Stopped SagerCaster device background discovery");
|
||||
@@ -117,7 +102,7 @@ public class SagerCasterDiscoveryService extends AbstractDiscoveryService {
|
||||
}
|
||||
|
||||
public void createResults(PointType location) {
|
||||
String propGeolocation = String.format("%s,%s", location.getLatitude(), location.getLongitude());
|
||||
String propGeolocation = "%s,%s".formatted(location.getLatitude(), location.getLongitude());
|
||||
thingDiscovered(DiscoveryResultBuilder.create(SAGER_CASTER_THING).withLabel("Local Weather Forecast")
|
||||
.withRepresentationProperty(CONFIG_LOCATION).withProperty(CONFIG_LOCATION, propGeolocation).build());
|
||||
}
|
||||
|
||||
+25
-13
@@ -12,8 +12,9 @@
|
||||
*/
|
||||
package org.openhab.binding.sagercaster.internal.handler;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.Optional;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@@ -26,24 +27,35 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class ExpiringMap<T> {
|
||||
private final SortedMap<Long, T> values = new TreeMap<>();
|
||||
private Optional<T> agedValue = Optional.empty();
|
||||
private long eldestAge = 0;
|
||||
private final NavigableMap<Long, T> values = new TreeMap<>();
|
||||
private final long windowMillis;
|
||||
|
||||
public void setObservationPeriod(long eldestAge) {
|
||||
this.eldestAge = eldestAge;
|
||||
public ExpiringMap(Duration duration) {
|
||||
this.windowMillis = duration.toMillis();
|
||||
}
|
||||
|
||||
public void put(T newValue) {
|
||||
/**
|
||||
* @param newValue - added to the Map
|
||||
* @return the eldest value if it existed before insertion
|
||||
*/
|
||||
public Optional<T> put(T newValue) {
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// removes all entries older than the configured window duration
|
||||
long cutoff = now - windowMillis;
|
||||
while (!values.isEmpty() && values.firstKey() < cutoff) {
|
||||
values.pollFirstEntry();
|
||||
}
|
||||
|
||||
values.put(now, newValue);
|
||||
values.keySet().stream().filter(key -> key < now - eldestAge).findFirst().ifPresent(eldest -> {
|
||||
agedValue = Optional.ofNullable(values.get(eldest));
|
||||
values.entrySet().removeIf(map -> map.getKey() <= eldest);
|
||||
});
|
||||
return Optional.ofNullable(values.size() < 2 ? null : values.firstEntry().getValue());
|
||||
}
|
||||
|
||||
public Optional<T> getAgedValue() {
|
||||
return agedValue;
|
||||
/**
|
||||
* @return age of the eldest element in minutes
|
||||
*/
|
||||
public long getDataAgeInMin() {
|
||||
long now = System.currentTimeMillis();
|
||||
return Duration.ofMillis(values.isEmpty() ? 0 : now - values.firstKey()).toMinutes();
|
||||
}
|
||||
}
|
||||
|
||||
+80
-75
@@ -15,33 +15,37 @@ package org.openhab.binding.sagercaster.internal.handler;
|
||||
import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstants.*;
|
||||
import static org.openhab.core.library.unit.MetricPrefix.HECTO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.measure.quantity.Angle;
|
||||
import javax.measure.quantity.Pressure;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.sagercaster.internal.WindDirectionStateDescriptionProvider;
|
||||
import org.openhab.binding.sagercaster.internal.caster.SagerWeatherCaster;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PercentType;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
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.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingStatus;
|
||||
import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -53,22 +57,22 @@ import org.slf4j.LoggerFactory;
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SagerCasterHandler extends BaseThingHandler {
|
||||
|
||||
private final static Duration ALGORITHM_WINDOW = Duration.ofHours(6);
|
||||
private final Logger logger = LoggerFactory.getLogger(SagerCasterHandler.class);
|
||||
|
||||
private final SagerWeatherCaster sagerWeatherCaster;
|
||||
|
||||
private final WindDirectionStateDescriptionProvider stateDescriptionProvider;
|
||||
|
||||
private final ExpiringMap<Double> pressureCache = new ExpiringMap<>();
|
||||
private final ExpiringMap<Double> temperatureCache = new ExpiringMap<>();
|
||||
private final ExpiringMap<Integer> bearingCache = new ExpiringMap<>();
|
||||
private final ExpiringMap<Double> pressureCache = new ExpiringMap<>(ALGORITHM_WINDOW);
|
||||
private final ExpiringMap<Double> temperatureCache = new ExpiringMap<>(ALGORITHM_WINDOW);
|
||||
private final ExpiringMap<Integer> bearingCache = new ExpiringMap<>(ALGORITHM_WINDOW);
|
||||
|
||||
private double currentTemp = 0;
|
||||
private String currentSagerCode = SagerWeatherCaster.UNDEF;
|
||||
private @Nullable String currentSagerCode;
|
||||
|
||||
public SagerCasterHandler(Thing thing, WindDirectionStateDescriptionProvider stateDescriptionProvider,
|
||||
SagerWeatherCaster sagerWeatherCaster) {
|
||||
public SagerCasterHandler(final Thing thing, final WindDirectionStateDescriptionProvider stateDescriptionProvider,
|
||||
final SagerWeatherCaster sagerWeatherCaster) {
|
||||
super(thing);
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.sagerWeatherCaster = sagerWeatherCaster;
|
||||
@@ -76,18 +80,19 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
int observationPeriod = ((BigDecimal) getConfig().get(CONFIG_PERIOD)).intValue();
|
||||
long period = TimeUnit.HOURS.toMillis(observationPeriod);
|
||||
pressureCache.setObservationPeriod(period);
|
||||
bearingCache.setObservationPeriod(period);
|
||||
temperatureCache.setObservationPeriod(period);
|
||||
|
||||
String location = (String) getConfig().get(CONFIG_LOCATION);
|
||||
String latitude = location.split(",")[0];
|
||||
sagerWeatherCaster.setLatitude(Double.parseDouble(latitude));
|
||||
defineWindDirectionStateDescriptions();
|
||||
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
if (getConfig().get(CONFIG_LOCATION) instanceof String locationString) {
|
||||
try {
|
||||
PointType location = new PointType(locationString);
|
||||
sagerWeatherCaster.setLatitude(location.getLatitude().doubleValue());
|
||||
defineWindDirectionStateDescriptions();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (IllegalArgumentException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"Location incorrectly configured");
|
||||
}
|
||||
} else {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Location incorrectly configured");
|
||||
}
|
||||
}
|
||||
|
||||
private void defineWindDirectionStateDescriptions() {
|
||||
@@ -95,7 +100,7 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
String[] directions = sagerWeatherCaster.getUsedDirections();
|
||||
for (int i = 0; i < directions.length; i++) {
|
||||
int secondDirection = i < directions.length - 1 ? i + 1 : 0;
|
||||
String windDescription = directions[i] + " or " + directions[secondDirection] + " winds";
|
||||
String windDescription = "%s or %s winds".formatted(directions[i], directions[secondDirection]);
|
||||
options.add(new StateOption(String.valueOf(i + 1), windDescription));
|
||||
}
|
||||
|
||||
@@ -105,13 +110,13 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, GROUP_OUTPUT, CHANNEL_WINDTO), options);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
postNewForecast();
|
||||
} else {
|
||||
String id = channelUID.getIdWithoutGroup();
|
||||
switch (id) {
|
||||
switch (channelUID.getIdWithoutGroup()) {
|
||||
case CHANNEL_CLOUDINESS:
|
||||
logger.debug("Cloud level changed, updating forecast");
|
||||
if (command instanceof QuantityType cloudiness) {
|
||||
@@ -119,13 +124,13 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
sagerWeatherCaster.setCloudLevel(cloudiness.intValue());
|
||||
postNewForecast();
|
||||
});
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case CHANNEL_IS_RAINING:
|
||||
logger.debug("Rain status updated, updating forecast");
|
||||
if (command instanceof OnOffType isRaining) {
|
||||
scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setRaining(isRaining == OnOffType.ON);
|
||||
sagerWeatherCaster.setRaining(OnOffType.ON.equals(isRaining));
|
||||
postNewForecast();
|
||||
});
|
||||
} else {
|
||||
@@ -134,12 +139,11 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
break;
|
||||
case CHANNEL_RAIN_QTTY:
|
||||
logger.debug("Rain status updated, updating forecast");
|
||||
if (command instanceof QuantityType quantityCommand) {
|
||||
updateRain(quantityCommand);
|
||||
} else if (command instanceof DecimalType decimalCommand) {
|
||||
updateRain(decimalCommand);
|
||||
} else {
|
||||
logger.debug("Channel '{}' accepts Number, Number:(Speed|Length) commands.", channelUID);
|
||||
switch (command) {
|
||||
case QuantityType<?> quantity -> updateRain(quantity);
|
||||
case DecimalType decimal -> updateRain(decimal);
|
||||
default ->
|
||||
logger.debug("Channel '{}' accepts Number, Number:(Speed|Length) commands.", channelUID);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_WIND_SPEED:
|
||||
@@ -156,13 +160,10 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
case CHANNEL_PRESSURE:
|
||||
logger.debug("Sea-level pressure updated, updating forecast");
|
||||
if (command instanceof QuantityType) {
|
||||
@SuppressWarnings("unchecked")
|
||||
QuantityType<Pressure> pressQtty = ((QuantityType<Pressure>) command)
|
||||
.toUnit(HECTO(SIUnits.PASCAL));
|
||||
if (pressQtty != null) {
|
||||
double newPressureValue = pressQtty.doubleValue();
|
||||
pressureCache.put(newPressureValue);
|
||||
pressureCache.getAgedValue().ifPresentOrElse(oldPressure -> scheduler.submit(() -> {
|
||||
QuantityType<?> pressure = ((QuantityType<Pressure>) command).toUnit(HECTO(SIUnits.PASCAL));
|
||||
if (pressure != null) {
|
||||
double newPressureValue = pressure.doubleValue();
|
||||
pressureCache.put(newPressureValue).ifPresentOrElse(oldPressure -> scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setPressure(newPressureValue, oldPressure);
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_PRESSURETREND,
|
||||
String.valueOf(sagerWeatherCaster.getPressureEvolution()));
|
||||
@@ -174,13 +175,10 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
case CHANNEL_TEMPERATURE:
|
||||
logger.debug("Temperature updated");
|
||||
if (command instanceof QuantityType) {
|
||||
@SuppressWarnings("unchecked")
|
||||
QuantityType<Temperature> tempQtty = ((QuantityType<Temperature>) command)
|
||||
.toUnit(SIUnits.CELSIUS);
|
||||
if (tempQtty != null) {
|
||||
currentTemp = tempQtty.doubleValue();
|
||||
temperatureCache.put(currentTemp);
|
||||
temperatureCache.getAgedValue().ifPresent(oldTemperature -> {
|
||||
QuantityType<?> temperature = ((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS);
|
||||
if (temperature != null) {
|
||||
currentTemp = temperature.doubleValue();
|
||||
temperatureCache.put(currentTemp).ifPresent(oldTemperature -> {
|
||||
double delta = currentTemp - oldTemperature;
|
||||
String trend = (delta > 3) ? "1"
|
||||
: (delta > 0.3) ? "2" : (delta > -0.3) ? "3" : (delta > -3) ? "4" : "5";
|
||||
@@ -191,19 +189,14 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
break;
|
||||
case CHANNEL_WIND_ANGLE:
|
||||
logger.debug("Updated wind direction, updating forecast");
|
||||
if (command instanceof QuantityType) {
|
||||
@SuppressWarnings("unchecked")
|
||||
QuantityType<Angle> angleQtty = (QuantityType<Angle>) command;
|
||||
int newAngleValue = angleQtty.intValue();
|
||||
bearingCache.put(newAngleValue);
|
||||
bearingCache.getAgedValue().ifPresent(oldAngle -> {
|
||||
scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setBearing(newAngleValue, oldAngle);
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDEVOLUTION,
|
||||
String.valueOf(sagerWeatherCaster.getWindEvolution()));
|
||||
postNewForecast();
|
||||
});
|
||||
});
|
||||
if (command instanceof QuantityType angle) {
|
||||
int newAngleValue = angle.intValue();
|
||||
bearingCache.put(newAngleValue).ifPresent(oldAngle -> scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setBearing(newAngleValue, oldAngle);
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDEVOLUTION,
|
||||
String.valueOf(sagerWeatherCaster.getWindEvolution()));
|
||||
postNewForecast();
|
||||
}));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
@@ -220,11 +213,11 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
}
|
||||
|
||||
private void postNewForecast() {
|
||||
String newSagerCode = sagerWeatherCaster.getSagerCode();
|
||||
if (!newSagerCode.equals(currentSagerCode)) {
|
||||
logger.debug("Sager prediction changed to {}", newSagerCode);
|
||||
currentSagerCode = newSagerCode;
|
||||
updateChannelTimeStamp(GROUP_OUTPUT, CHANNEL_TIMESTAMP, ZonedDateTime.now());
|
||||
String newCode = sagerWeatherCaster.getSagerCode();
|
||||
if (!Objects.equals(newCode, currentSagerCode)) {
|
||||
logger.debug("Sager prediction changed to {}", newCode);
|
||||
currentSagerCode = newCode;
|
||||
updateChannelTimeStamp(GROUP_OUTPUT, CHANNEL_TIMESTAMP, Instant.now());
|
||||
String forecast = sagerWeatherCaster.getForecast();
|
||||
// Sharpens forecast if current temp is below 2 degrees, likely to be flurries rather than shower
|
||||
forecast += SHOWERS.contains(forecast) ? (currentTemp > 2) ? "1" : "2" : "";
|
||||
@@ -234,27 +227,39 @@ public class SagerCasterHandler extends BaseThingHandler {
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDTO, sagerWeatherCaster.getWindDirection2());
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_VELOCITY, sagerWeatherCaster.getWindVelocity());
|
||||
updateChannelDecimal(GROUP_OUTPUT, CHANNEL_VELOCITY_BEAUFORT, sagerWeatherCaster.getPredictedBeaufort());
|
||||
updateReliability(GROUP_OUTPUT, CHANNEL_RELIABILITY);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannelTimeStamp(String group, String channelId, ZonedDateTime zonedDateTime) {
|
||||
private @Nullable ChannelUID getChannelId(String group, String channelId) {
|
||||
ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
|
||||
if (isLinked(id)) {
|
||||
updateState(id, new DateTimeType(zonedDateTime));
|
||||
return isLinked(id) ? id : null;
|
||||
}
|
||||
|
||||
private void updateChannelTimeStamp(String group, String channelId, Instant instant) {
|
||||
if (getChannelId(group, channelId) instanceof ChannelUID id) {
|
||||
updateState(id, new DateTimeType(instant));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannelString(String group, String channelId, String value) {
|
||||
ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
|
||||
if (isLinked(id)) {
|
||||
updateState(id, new StringType(value));
|
||||
private void updateChannelString(String group, String channelId, @Nullable String value) {
|
||||
if (getChannelId(group, channelId) instanceof ChannelUID id) {
|
||||
updateState(id, value != null ? new StringType(value) : UnDefType.NULL);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannelDecimal(String group, String channelId, int value) {
|
||||
ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
|
||||
if (isLinked(id)) {
|
||||
if (getChannelId(group, channelId) instanceof ChannelUID id) {
|
||||
updateState(id, new DecimalType(value));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateReliability(String group, String channelId) {
|
||||
if (getChannelId(group, channelId) instanceof ChannelUID id) {
|
||||
long minDuration = Math.min(bearingCache.getDataAgeInMin(),
|
||||
Math.min(pressureCache.getDataAgeInMin(), temperatureCache.getDataAgeInMin()));
|
||||
double ratio = minDuration > 0 ? Math.min(1.0, minDuration / (ALGORITHM_WINDOW.toMinutes() * 1.0)) : 0.0;
|
||||
updateState(id, new PercentType((int) Math.round(ratio * 100.0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -50,6 +50,8 @@ beaufortDescription = Wind speed using Beaufort Scale
|
||||
pressureDescription = Barometric pressure at sea level.
|
||||
timestampChannelLabel = Timestamp
|
||||
timestampChannelDescription = Timestamp of the last weather forecast update.
|
||||
reliabilityLabel = Algorithm reliability
|
||||
reliabilityDescription = Grows along with the algorithm historical data storage (up to 6 hours)
|
||||
|
||||
# channel options
|
||||
|
||||
|
||||
+16
-6
@@ -13,6 +13,10 @@
|
||||
<channel-group id="output" typeId="output"/>
|
||||
</channel-groups>
|
||||
|
||||
<properties>
|
||||
<property name="thingTypeVersion">1</property>
|
||||
</properties>
|
||||
|
||||
<representation-property>location</representation-property>
|
||||
|
||||
<config-description>
|
||||
@@ -23,12 +27,6 @@
|
||||
<description>@text/locationDescription</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="observation-period" type="integer" min="0" max="6" required="true">
|
||||
<label>@text/observationLabel</label>
|
||||
<description>@text/observationDescription</description>
|
||||
<default>6</default>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
@@ -73,6 +71,7 @@
|
||||
<description>@text/tempTrendDescription</description>
|
||||
</channel>
|
||||
<channel id="timestamp" typeId="timestamp"/>
|
||||
<channel id="reliability" typeId="reliability"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
@@ -239,4 +238,15 @@
|
||||
<state min="0" max="12" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="reliability" advanced="true">
|
||||
<item-type unitHint="%">Number:Dimensionless</item-type>
|
||||
<label>@text/reliabilityLabel</label>
|
||||
<description>@text/reliabilityDescription</description>
|
||||
<tags>
|
||||
<tag>Calculation</tag>
|
||||
</tags>
|
||||
<state min="0" step="0.1" max="100" pattern="%.0f %unit%" readOnly="true"/>
|
||||
<autoUpdatePolicy>veto</autoUpdatePolicy>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
|
||||
<update:update-descriptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:update="https://openhab.org/schemas/update-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/update-description/v1.0.0 https://openhab.org/schemas/update-description-1.0.0.xsd">
|
||||
|
||||
<thing-type uid="sagercaster:sagercaster">
|
||||
|
||||
<instruction-set targetVersion="1">
|
||||
<add-channel id="reliability" groupIds="output">
|
||||
<type>sagercaster:reliability</type>
|
||||
</add-channel>
|
||||
</instruction-set>
|
||||
|
||||
</thing-type>
|
||||
|
||||
</update:update-descriptions>
|
||||
Reference in New Issue
Block a user