[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:
Gaël L'hopital
2025-10-31 14:07:08 +01:00
committed by GitHub
parent 66264a66b7
commit 21f3a7ecd1
11 changed files with 245 additions and 261 deletions
@@ -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.
@@ -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";
@@ -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;
}
@@ -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;
}
}
@@ -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;
};
}
}
@@ -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());
}
@@ -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();
}
}
@@ -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)));
}
}
}
@@ -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
@@ -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>
@@ -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>