[synopanalyzer] Add IconProvider (#20193)

* Add IconProvider

Signed-off-by: gael@lhopital.org <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital
2026-03-06 11:16:45 +01:00
committed by GitHub
parent 4f99334ab3
commit 30dbdea1e9
36 changed files with 475 additions and 16 deletions
@@ -19,3 +19,11 @@ Based on the commit bed5842 / 3 May 2017
* License: EPL + MIT License * License: EPL + MIT License
* Project: https://github.com/Jonarzz/SynopAnalizer * Project: https://github.com/Jonarzz/SynopAnalizer
* Source: https://github.com/Jonarzz/SynopAnalizer * Source: https://github.com/Jonarzz/SynopAnalizer
Wind barbs symbols
* License: Creative Commons
* Source: https://commons.wikimedia.org/wiki/Wind_speed
Cloud Cover in Oktas
* License: Creative Commons
* Source: https://commons.wikimedia.org/wiki/File:Cloud_Cover_in_Oktas.svg
@@ -15,6 +15,15 @@ There is exactly one supported Thing type, which represents a SYNOP message: `sy
If a system location is set, the nearest available SYNOP station will be automatically discovered for this location. If a system location is set, the nearest available SYNOP station will be automatically discovered for this location.
The search radius expands with each successive scan. The search radius expands with each successive scan.
## Provided icon set
This binding has its own IconProvider and makes available the following list of icons
| Icon Name | Dynamic | Illustration |
| --------------------------- | ------- | ------------------------------------------------- |
| oh:synopanalyzer:beaufort | Yes | ![Beaufort](doc/images/beaufort.svg) |
| oh:synopanalyzer:octa | Yes | ![Octa](doc/images/octa.svg) |
## Thing Configuration ## Thing Configuration
- `stationId` — The WMO station number (see the [station list](https://www.ogimet.com/gsynop_nav.phtml.en)). - `stationId` — The WMO station number (see the [station list](https://www.ogimet.com/gsynop_nav.phtml.en)).
@@ -39,6 +48,7 @@ The weather information that is retrieved is available as these channels:
| attenuation-factor* | Number | Cloud layer attenuation factor | | attenuation-factor* | Number | Cloud layer attenuation factor |
| time-utc | DateTime | Observation time of the Synop message | | time-utc | DateTime | Observation time of the Synop message |
| horizontal-visibility | String | Horizontal visibility range | | horizontal-visibility | String | Horizontal visibility range |
| wind-barb | Image | Graph of the wind speed and direction |
- "Cloud attenuation factor" (Kc) as defined by Kasten and Czeplak (1980). - "Cloud attenuation factor" (Kc) as defined by Kasten and Czeplak (1980).
@@ -60,6 +70,48 @@ Number:Pressure Synop_Pressure "Pressure [%.1f %unit%]" <pressure> {
Number:Angle Synop_Wind_Angle "Wind Angle [%d °]" <wind> { channel = "synopanalyzer:synopanalyzer:trappes:wind-angle" } Number:Angle Synop_Wind_Angle "Wind Angle [%d °]" <wind> { channel = "synopanalyzer:synopanalyzer:trappes:wind-angle" }
String Synop_Wind_Direction "Direction [%s]" { channel = "synopanalyzer:synopanalyzer:trappes:wind-direction" } String Synop_Wind_Direction "Direction [%s]" { channel = "synopanalyzer:synopanalyzer:trappes:wind-direction" }
Number:Speed Synop_Wind_Speed "Wind Speed [%.2f %unit%]" <wind> { channel = "synopanalyzer:synopanalyzer:trappes:wind-speed" } Number:Speed Synop_Wind_Speed "Wind Speed [%.2f %unit%]" <wind> { channel = "synopanalyzer:synopanalyzer:trappes:wind-speed" }
Number Synop_Octa "Octa [%d]/8" { channel = "synopanalyzer:synopanalyzer:trappes:octa" } Number Synop_Octa "Octa [%d]/8" <oh:synopanalyzer:octa> { channel = "synopanalyzer:synopanalyzer:trappes:octa" }
DateTime Synop_time "Observation Time [%1$ta %1$tR]" <clock> { channel = "synopanalyzer:synopanalyzer:trappes:time-utc" } DateTime Synop_time "Observation Time [%1$ta %1$tR]" <clock> { channel = "synopanalyzer:synopanalyzer:trappes:time-utc" }
``` ```
### Transformations
octa.map
```text
0=○ No clouds
1=⌽ A few clouds
2=◔ A few clouds
3=◑ Scattered clouds
4=◑ Scattered clouds
5=◕ Broken sky
6=◕ Broken sky
7=◕ Broken sky
8=● Overcast sky
9=⊗ Obscured sky
UNDEF=Unknown ⁉
NULL=Unknown ⁉
-=Unknown ⁉
=Unknown ⁉
```
beaufort.scale
```text
[0..1[=Calm
[1..2[=Very light breeze
[2..3[=Light breeze
[3..4[=Gentle breeze
[4..5[=Moderate breeze
[5..6[=Fresh breeze
[6..7[=Strong breeze
[7..8[=Near gale
[8..9[=Gale
[9..10[=Strong gale
[10..11[=Storm
[11..12[=Violent storm
[12..15[=Hurricane
[..]=Unknown ⁉
NaN=Not initialized (NaN)
```
@@ -0,0 +1,6 @@
<svg width="64" height="64" viewBox="-25 -25 50 50" xmlns="http://www.w3.org/2000/svg">
<path d="M-22 9.14v-1l5-17.25 5 17.25v1" fill="#ccc" stroke="#ccc" stroke-opacity=".8" />
<path d="M22 8.141h-40m10 0l-5-17" fill="none" stroke="#ccc" stroke-width="2" /><text style="white-space:pre"
transform="scale(1.02258 .97792)" x="-7.708" y="12.019" fill="#999" font-family="'Noto Sans'" font-size="33.809"
font-weight="700" stroke-width=".638">?</text>
</svg>

After

Width:  |  Height:  |  Size: 472 B

@@ -0,0 +1,6 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" /><text style="white-space:pre"
transform="matrix(6.23986 0 0 6.03723 -1447.916 -1416.396)" x="259.868" y="296.998" fill="rgba(0,0,0,.27)"
font-family="Noto Sans" font-size="53" font-weight="700">?</text>
</svg>

After

Width:  |  Height:  |  Size: 456 B

@@ -31,7 +31,7 @@ import org.openhab.core.thing.ThingTypeUID;
*/ */
@NonNullByDefault @NonNullByDefault
public class SynopAnalyzerBindingConstants { public class SynopAnalyzerBindingConstants {
private static final String BINDING_ID = "synopanalyzer"; public static final String BINDING_ID = "synopanalyzer";
// List of all Thing Type UIDs // List of all Thing Type UIDs
public static final ThingTypeUID THING_SYNOP = new ThingTypeUID(BINDING_ID, "synopanalyzer"); public static final ThingTypeUID THING_SYNOP = new ThingTypeUID(BINDING_ID, "synopanalyzer");
@@ -48,6 +48,7 @@ public class SynopAnalyzerBindingConstants {
public static final String WIND_STRENGTH = "wind-speed"; public static final String WIND_STRENGTH = "wind-speed";
public static final String WIND_SPEED_BEAUFORT = "wind-speed-beaufort"; public static final String WIND_SPEED_BEAUFORT = "wind-speed-beaufort";
public static final String TIME_UTC = "time-utc"; public static final String TIME_UTC = "time-utc";
public static final String WIND_BARB = "wind-barb";
// Default units // Default units
public static final Unit<Temperature> TEMPERATURE_UNIT = SIUnits.CELSIUS; public static final Unit<Temperature> TEMPERATURE_UNIT = SIUnits.CELSIUS;
@@ -0,0 +1,112 @@
/*
* Copyright (c) 2010-2026 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.synopanalyzer.internal;
import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.BINDING_ID;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Locale;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.ui.icon.IconProvider;
import org.openhab.core.ui.icon.IconSet;
import org.openhab.core.ui.icon.IconSet.Format;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SynopAnalyzerIconProvider} is the class providing binding related icons.
*
* @author Gaël L'hopital - Initial contribution
*/
@Component(service = { IconProvider.class, SynopAnalyzerIconProvider.class })
@NonNullByDefault
public class SynopAnalyzerIconProvider implements IconProvider {
private static final String DEFAULT_LABEL = "Synop Analyzer Icons";
private static final String DEFAULT_DESCRIPTION = "Icons provided for the Synop Analyzer Binding";
private static final String BEAUFORT_SET = "beaufort";
private static final String OCTA_SET = "octa";
private static final Set<String> ICON_SETS = Set.of(BEAUFORT_SET, OCTA_SET);
private final Logger logger = LoggerFactory.getLogger(SynopAnalyzerIconProvider.class);
private final TranslationProvider i18nProvider;
private final Bundle bundle;
@Activate
public SynopAnalyzerIconProvider(final BundleContext context, final @Reference TranslationProvider i18nProvider) {
this.i18nProvider = i18nProvider;
this.bundle = context.getBundle();
}
@Override
public Set<IconSet> getIconSets() {
return getIconSets(null);
}
@Override
public Set<IconSet> getIconSets(@Nullable Locale locale) {
String label = getText("label", DEFAULT_LABEL, locale);
String description = getText("description", DEFAULT_DESCRIPTION, locale);
return Set.of(new IconSet(BINDING_ID, label, description, Set.of(Format.SVG)));
}
private String getText(String entry, String defaultValue, @Nullable Locale locale) {
String text = locale == null ? null : i18nProvider.getText(bundle, "iconset." + entry, defaultValue, locale);
return text == null ? defaultValue : text;
}
@Override
public @Nullable Integer hasIcon(String category, String iconSetId, Format format) {
return Format.SVG.equals(format) && iconSetId.equals(BINDING_ID) && ICON_SETS.contains(category) ? 0 : null;
}
@Override
public @Nullable InputStream getIcon(String category, String iconSetId, @Nullable String state, Format format) {
String resourceWithoutState = "icon/" + category + "." + format.toString();
if (state == null) {
return getResource(resourceWithoutState);
}
try {
String resourceWithState = "icon/" + category + "-" + state + "." + format.toString();
return getResource(resourceWithState);
} catch (IllegalArgumentException e) {
logger.debug("Use icon {} as state {} is not found", resourceWithoutState, state);
return getResource(resourceWithoutState);
}
}
private @Nullable InputStream getResource(String iconName) {
if (bundle.getEntry(iconName.toLowerCase(Locale.ROOT)) instanceof URL iconResource) {
try (InputStream stream = iconResource.openStream()) {
byte[] icon = stream.readAllBytes();
return new ByteArrayInputStream(icon);
} catch (IOException e) {
logger.warn("Unable to load resource '{}': {}", iconResource.getPath(), e.getMessage());
}
}
return null;
}
}
@@ -0,0 +1,104 @@
/*
* Copyright (c) 2010-2026 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.synopanalyzer.internal;
import java.util.Locale;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link WindBarbGenerator} generates a SVG representation of wind speed
* and direction
* Inspired by https://github.com/spatialsparks/Leaflet.windbarb
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class WindBarbGenerator {
private static final String FILL_COLOR = "#2B85C7";
private static final int POINT_RADIUS = 4; // station circle radius
private static final int STROKE_WIDTH = 1;
private static final int STROKE_LENGTH = 45; // hamp length
private static final int BARB_SPACING = 5;
private static final int BARB_HEIGHT = 15;
private static final int VIEW_SIZE = 120;
public String generateSVG(double speedKnots, double directionDeg) {
int s = (int) (5 * Math.round(speedKnots / 5.0));
int f50 = s / 50;
int f10 = (s % 50) / 10;
int f5 = (s % 10) / 5;
double mid = VIEW_SIZE / 2.0;
StringBuilder sb = new StringBuilder();
sb.append("<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 ").append(VIEW_SIZE).append(" ")
.append(VIEW_SIZE).append("' width='").append(VIEW_SIZE).append("' height='").append(VIEW_SIZE)
.append("'>");
// Rotation depending on wind direction
sb.append("<g transform='rotate(").append(directionDeg).append(", ").append(mid).append(", ").append(mid)
.append(")'>");
// Station circle
sb.append("<circle cx='").append(mid).append("' cy='").append(mid).append("' r='").append(POINT_RADIUS)
.append("' fill='").append(FILL_COLOR).append("' stroke='black' stroke-width='").append(STROKE_WIDTH)
.append("'/>");
// Hamp
double lineTopY = mid - POINT_RADIUS - STROKE_LENGTH;
sb.append("<line x1='").append(mid).append("' y1='").append(mid - POINT_RADIUS).append("' x2='").append(mid)
.append("' y2='").append(lineTopY).append("' stroke='black' stroke-width='").append(STROKE_WIDTH)
.append("' stroke-linecap='butt'/>");
// Start at the end of the hamp
double currentY = lineTopY;
for (int i = 0; i < f50; i++) { // 50 kt pennants
currentY += BARB_SPACING;
String points = String.format(Locale.US, "%.1f,%.1f %.1f,%.1f %.1f,%.1f", mid, currentY, mid + BARB_HEIGHT,
currentY - BARB_SPACING, mid, currentY - BARB_SPACING);
sb.append("<polygon points='").append(points).append("' fill='black' stroke='black' ")
.append("stroke-width='").append(STROKE_WIDTH * 1).append("' stroke-linejoin='round'/>");
currentY += 2;
}
if (f50 > 0) {
currentY += (BARB_SPACING / 2.0);
}
for (int i = 0; i < f10; i++) { // 10 kt barbs
appendBarb(sb, mid, currentY, BARB_HEIGHT, BARB_SPACING);
currentY += BARB_SPACING;
}
if (f5 == 1) { // 5 kt barb
if (s == 5) {
currentY += BARB_SPACING;
}
appendBarb(sb, mid, currentY, BARB_HEIGHT * 0.5, BARB_SPACING * 0.5);
}
sb.append("</g></svg>");
return sb.toString();
}
private void appendBarb(StringBuilder sb, double mid, double currentY, double barbHeight, double barbSpacing) {
sb.append("<line x1='").append(mid).append("' y1='").append(currentY).append("' x2='").append(mid + barbHeight)
.append("' y2='").append(currentY - barbSpacing).append("' stroke='black' stroke-width='")
.append(STROKE_WIDTH).append("' stroke-linecap='round'/>");
}
}
@@ -89,7 +89,7 @@ public class SynopAnalyzerDiscoveryService extends AbstractDiscoveryService {
radius = nearest.getKey(); radius = nearest.getKey();
thingDiscovered(DiscoveryResultBuilder.create(new ThingUID(THING_SYNOP, Integer.toString(station.idOmm))) thingDiscovered(DiscoveryResultBuilder.create(new ThingUID(THING_SYNOP, Integer.toString(station.idOmm)))
.withLabel(String.format("Synop : %s", station.usualName)) .withLabel(String.format("Synop: %s", station.usualName))
.withProperty(SynopAnalyzerConfiguration.STATION_ID, station.idOmm) .withProperty(SynopAnalyzerConfiguration.STATION_ID, station.idOmm)
.withRepresentationProperty(SynopAnalyzerConfiguration.STATION_ID).build()); .withRepresentationProperty(SynopAnalyzerConfiguration.STATION_ID).build());
} else { } else {
@@ -15,6 +15,7 @@ package org.openhab.binding.synopanalyzer.internal.handler;
import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.*; import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.*;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
@@ -23,6 +24,7 @@ import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@@ -32,6 +34,7 @@ import javax.ws.rs.HttpMethod;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.synopanalyzer.internal.WindBarbGenerator;
import org.openhab.binding.synopanalyzer.internal.config.SynopAnalyzerConfiguration; import org.openhab.binding.synopanalyzer.internal.config.SynopAnalyzerConfiguration;
import org.openhab.binding.synopanalyzer.internal.stationdb.Station; import org.openhab.binding.synopanalyzer.internal.stationdb.Station;
import org.openhab.binding.synopanalyzer.internal.synop.Overcast; import org.openhab.binding.synopanalyzer.internal.synop.Overcast;
@@ -46,8 +49,10 @@ import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PointType; import org.openhab.core.library.types.PointType;
import org.openhab.core.library.types.QuantityType; import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.RawType;
import org.openhab.core.library.types.StringType; import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits; import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
@@ -75,6 +80,7 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
private static final String OGIMET_SYNOP_PATH = "http://www.ogimet.com/cgi-bin/getsynop?block=%s&begin=%s"; private static final String OGIMET_SYNOP_PATH = "http://www.ogimet.com/cgi-bin/getsynop?block=%s&begin=%s";
private static final int REQUEST_TIMEOUT_MS = 5000; private static final int REQUEST_TIMEOUT_MS = 5000;
private static final DateTimeFormatter SYNOP_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHH00"); private static final DateTimeFormatter SYNOP_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHH00");
private static final WindBarbGenerator WB_GENERATOR = new WindBarbGenerator();
private static final double KASTEN_POWER = 3.4; private static final double KASTEN_POWER = 3.4;
private static final double OCTA_MAX = 8.0; private static final double OCTA_MAX = 8.0;
@@ -82,7 +88,7 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
private final LocationProvider locationProvider; private final LocationProvider locationProvider;
private final List<Station> stations; private final List<Station> stations;
private Optional<ScheduledFuture<?>> refreshJob = Optional.empty(); private @Nullable ScheduledFuture<?> refreshJob;
private @NonNullByDefault({}) String formattedStationId; private @NonNullByDefault({}) String formattedStationId;
public SynopAnalyzerHandler(Thing thing, LocationProvider locationProvider, List<Station> stations) { public SynopAnalyzerHandler(Thing thing, LocationProvider locationProvider, List<Station> stations) {
@@ -104,8 +110,8 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
updateStatus(ThingStatus.UNKNOWN); updateStatus(ThingStatus.UNKNOWN);
refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::updateChannels, 0, refreshJob = scheduler.scheduleWithFixedDelay(this::updateChannels, 0, configuration.refreshInterval,
configuration.refreshInterval, TimeUnit.MINUTES)); TimeUnit.MINUTES);
} }
private void discoverAttributes(int stationId, @Nullable PointType serverLocation) { private void discoverAttributes(int stationId, @Nullable PointType serverLocation) {
@@ -131,13 +137,13 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
String answer = HttpUtil.executeUrl(HttpMethod.GET, url, REQUEST_TIMEOUT_MS); String answer = HttpUtil.executeUrl(HttpMethod.GET, url, REQUEST_TIMEOUT_MS);
List<String> messages = Arrays.asList(answer.split("\n")); List<String> messages = Arrays.asList(answer.split("\n"));
if (!messages.isEmpty()) { if (!messages.isEmpty()) {
String message = messages.get(messages.size() - 1); String message = messages.getLast();
logger.debug(message); logger.debug(message);
if (message.startsWith(formattedStationId)) { if (message.startsWith(formattedStationId)) {
logger.debug("Valid Synop message received"); logger.debug("Valid Synop message received");
List<String> messageParts = Arrays.asList(message.split(",")); List<String> messageParts = Arrays.asList(message.split(","));
String synopMessage = messageParts.get(messageParts.size() - 1); String synopMessage = messageParts.getLast();
return createSynopObject(synopMessage); return createSynopObject(synopMessage);
} }
@@ -167,6 +173,7 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
private State getChannelState(String channelId, Synop synop) { private State getChannelState(String channelId, Synop synop) {
int octa = synop.getOcta(); int octa = synop.getOcta();
Integer direction = synop.getWindDirection(); Integer direction = synop.getWindDirection();
QuantityType<Speed> windSpeed = getWindStrength(synop);
switch (channelId) { switch (channelId) {
case HORIZONTAL_VISIBILITY: case HORIZONTAL_VISIBILITY:
return new StringType(synop.getHorizontalVisibility().name()); return new StringType(synop.getHorizontalVisibility().name());
@@ -181,7 +188,7 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
return UnDefType.NULL; return UnDefType.NULL;
case OVERCAST: case OVERCAST:
Overcast overcast = Overcast.fromOcta(octa); Overcast overcast = Overcast.fromOcta(octa);
return overcast == Overcast.UNDEFINED ? UnDefType.NULL : new StringType(overcast.name()); return Overcast.UNDEFINED.equals(overcast) ? UnDefType.NULL : new StringType(overcast.name());
case PRESSURE: case PRESSURE:
return new QuantityType<>(synop.getPressure(), PRESSURE_UNIT); return new QuantityType<>(synop.getPressure(), PRESSURE_UNIT);
case TEMPERATURE: case TEMPERATURE:
@@ -193,10 +200,16 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
: UnDefType.NULL; : UnDefType.NULL;
case WIND_STRENGTH: case WIND_STRENGTH:
return getWindStrength(synop); return getWindStrength(synop);
case WIND_BARB:
if (direction == null) {
return UnDefType.NULL;
}
QuantityType<Speed> wsKnot = Objects.requireNonNull(windSpeed.toUnit(Units.KNOT));
String windBarbs = WB_GENERATOR.generateSVG(wsKnot.doubleValue(), direction);
return new RawType(windBarbs.getBytes(StandardCharsets.UTF_8), "image/svg+xml");
case WIND_SPEED_BEAUFORT: case WIND_SPEED_BEAUFORT:
QuantityType<Speed> wsKpH = getWindStrength(synop).toUnit(SIUnits.KILOMETRE_PER_HOUR); QuantityType<Speed> wsKpH = Objects.requireNonNull(windSpeed.toUnit(SIUnits.KILOMETRE_PER_HOUR));
return wsKpH != null ? new DecimalType(Math.round(Math.pow(wsKpH.floatValue() / 3.01, 0.666666666))) return new DecimalType(Math.round(Math.pow(wsKpH.floatValue() / 3.01, 0.666666666)));
: UnDefType.NULL;
case TIME_UTC: case TIME_UTC:
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
int year = synop.getYear() == 0 ? now.getYear() : synop.getYear(); int year = synop.getYear() == 0 ? now.getYear() : synop.getYear();
@@ -236,8 +249,10 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
@Override @Override
public void dispose() { public void dispose() {
refreshJob.ifPresent(job -> job.cancel(true)); if (refreshJob instanceof ScheduledFuture job) {
refreshJob = Optional.empty(); job.cancel(true);
}
refreshJob = null;
} }
@Override @Override
@@ -34,6 +34,8 @@ channel-type.synopanalyzer.overcast.state.option.CLOUDY = Cloudy
channel-type.synopanalyzer.overcast.state.option.SKY_NOT_VISIBLE = Sky is not visible channel-type.synopanalyzer.overcast.state.option.SKY_NOT_VISIBLE = Sky is not visible
channel-type.synopanalyzer.time-utc.label = Observation Time channel-type.synopanalyzer.time-utc.label = Observation Time
channel-type.synopanalyzer.time-utc.description = Timestamp when data was observed channel-type.synopanalyzer.time-utc.description = Timestamp when data was observed
channel-type.synopanalyzer.wind-barb.label = Wind Barb
channel-type.synopanalyzer.wind-barb.description = Graph of the wind speed and direction
channel-type.synopanalyzer.wind-direction.label = Wind Direction channel-type.synopanalyzer.wind-direction.label = Wind Direction
channel-type.synopanalyzer.wind-direction.description = Wind direction channel-type.synopanalyzer.wind-direction.description = Wind direction
channel-type.synopanalyzer.wind-direction.state.option.N = N channel-type.synopanalyzer.wind-direction.state.option.N = N
@@ -54,3 +56,8 @@ channel-type.synopanalyzer.wind-direction.state.option.NW = NW
channel-type.synopanalyzer.wind-direction.state.option.NNW = NNW channel-type.synopanalyzer.wind-direction.state.option.NNW = NNW
channel-type.synopanalyzer.wind-speed-beaufort.label = Beaufort channel-type.synopanalyzer.wind-speed-beaufort.label = Beaufort
channel-type.synopanalyzer.wind-speed-beaufort.description = Wind speed in Beaufort Scale channel-type.synopanalyzer.wind-speed-beaufort.description = Wind speed in Beaufort Scale
# iconprovider
iconset.label = Synop Analyzer Icons
iconset.description = Icons provided for the Synop Analyzer Binding
@@ -20,6 +20,7 @@
<channel id="attenuation-factor" typeId="attenuation-factor"/> <channel id="attenuation-factor" typeId="attenuation-factor"/>
<channel id="horizontal-visibility" typeId="horizontal-visibility"/> <channel id="horizontal-visibility" typeId="horizontal-visibility"/>
<channel id="time-utc" typeId="time-utc"/> <channel id="time-utc" typeId="time-utc"/>
<channel id="wind-barb" typeId="wind-barb"/>
</channels> </channels>
<config-description> <config-description>
@@ -41,7 +42,7 @@
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Beaufort</label> <label>Beaufort</label>
<description>Wind speed in Beaufort Scale</description> <description>Wind speed in Beaufort Scale</description>
<category>Wind</category> <category>oh:synopanalyzer:beaufort</category>
<tags> <tags>
<tag>Calculation</tag> <tag>Calculation</tag>
<tag>Speed</tag> <tag>Speed</tag>
@@ -84,7 +85,7 @@
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Octa</label> <label>Octa</label>
<description>Cloud cover estimation.</description> <description>Cloud cover estimation.</description>
<category>sun_clouds</category> <category>oh:synopanalyzer:octa</category>
<state readOnly="true" pattern="%d/8" min="0" max="8"/> <state readOnly="true" pattern="%d/8" min="0" max="8"/>
</channel-type> </channel-type>
@@ -93,6 +94,9 @@
<label>Attenuation Factor</label> <label>Attenuation Factor</label>
<description>Cloud layer attenuation factor</description> <description>Cloud layer attenuation factor</description>
<category>sun_clouds</category> <category>sun_clouds</category>
<tags>
<tag>Calculation</tag>
</tags>
<state readOnly="true" pattern="%.1f" max="1" min="0"/> <state readOnly="true" pattern="%.1f" max="1" min="0"/>
</channel-type> </channel-type>
@@ -129,6 +133,17 @@
<label>Observation Time</label> <label>Observation Time</label>
<description>Timestamp when data was observed</description> <description>Timestamp when data was observed</description>
<category>time</category> <category>time</category>
<tags>
<tag>Status</tag>
<tag>Timestamp</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="wind-barb">
<item-type>Image</item-type>
<label>Wind Barb</label>
<description>Graph of the wind speed and direction</description>
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </channel-type>
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-25 -25 50 50">
<circle r="18" fill="none" stroke="#000" stroke-width="2.5" />
</svg>

After

Width:  |  Height:  |  Size: 160 B

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg width="64" height="64" viewBox="-48 -33 50 50" xmlns="http://www.w3.org/2000/svg">
<path d="M 0 0 L -40 0 M -35 0 L -29.986 -0.03" fill="none" stroke="#000" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 228 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<path d="M-44 1V0l5-17.25L-34 0v1" />
<path d="M0 0h-40" fill="none" stroke="#000" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 202 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<path d="M-44 1V0l5-17.25L-34 0v1" />
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40M-30 0l-5-17" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 228 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<path d="M-44 1V0l5-17.25L-34 0v1" />
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40M-30 0l-5-17M-25 0l-2.5-8.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 243 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40M-35 0l-2.5-8.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 191 B

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<path d="M0 0h-40l-5-17" fill="none" stroke="#000" stroke-width="2" />
</svg>

After

Width:  |  Height:  |  Size: 168 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40l-5-17M-35 0l-2.5-8.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 197 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40l-5-17M-35 0l-5-17" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 194 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40l-5-17M-35 0l-5-17M-30 0l-2.5-8.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 209 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40l-5-17M-35 0l-5-17M-30 0l-5-17" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 206 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40l-5-17M-35 0l-5-17M-30 0l-5-17M-25 0l-2.5-8.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 221 B

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="-48 -33 50 50">
<g fill="none" stroke="#000" stroke-width="2">
<path d="M0 0h-40l-5-17M-35 0l-5-17M-30 0l-5-17M-25 0l-5-17M-20 0l-2.5-8.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 233 B

@@ -0,0 +1,6 @@
<svg width="64" height="64" viewBox="-25 -25 50 50" xmlns="http://www.w3.org/2000/svg">
<path d="M-22 9.14v-1l5-17.25 5 17.25v1" fill="#ccc" stroke="#ccc" stroke-opacity=".8" />
<path d="M22 8.141h-40m10 0l-5-17" fill="none" stroke="#ccc" stroke-width="2" /><text style="white-space:pre"
transform="scale(1.02258 .97792)" x="-7.708" y="12.019" fill="#999" font-family="'Noto Sans'" font-size="33.809"
font-weight="700" stroke-width=".638">?</text>
</svg>

After

Width:  |  Height:  |  Size: 472 B

@@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" />
</svg>

After

Width:  |  Height:  |  Size: 246 B

@@ -0,0 +1,5 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" />
<path d="M252.5 7.5v490" fill="#d8d8d8" stroke="#000" stroke-width="10" />
</svg>

After

Width:  |  Height:  |  Size: 323 B

@@ -0,0 +1,5 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" />
<path d="M253.52 7.34c134.563.152 248.14 110.661 243.98 245.16l-245.064.823.064-.823V7.5" stroke="#000" />
</svg>

After

Width:  |  Height:  |  Size: 355 B

@@ -0,0 +1,6 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" />
<path d="M253.52 7.34c134.563.152 248.14 110.661 243.98 245.16l-245.064.823.064-.823V7.5" stroke="#000" />
<path d="M252.5 7.5v490" fill="#d8d8d8" stroke="#000" stroke-width="10" />
</svg>

After

Width:  |  Height:  |  Size: 432 B

@@ -0,0 +1,7 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" />
<path d="M253.52 7.34c134.563.152 248.14 110.661 243.98 245.16l-245.064.823.064-.823V7.5" stroke="#000" />
<path d="M252.5 7.5v490" fill="#d8d8d8" stroke="#000" stroke-width="10" />
<path d="M252.116 496.393c134.563-.152 248.14-110.661 243.98-245.16l-245.064-.823.064.823v245" stroke="#000" />
</svg>

After

Width:  |  Height:  |  Size: 546 B

@@ -0,0 +1,8 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" />
<path d="M253.52 7.34c134.563.152 248.14 110.661 243.98 245.16l-245.064.823.064-.823V7.5" stroke="#000" />
<path d="M252.5 7.5v490" fill="#d8d8d8" stroke="#000" stroke-width="10" />
<path d="M252.116 496.393c134.563-.152 248.14-110.661 243.98-245.16l-245.064-.823.064.823v245" stroke="#000" />
<path d="M7.5 252.5h490" fill="#d8d8d8" stroke="#000" stroke-width="10" />
</svg>

After

Width:  |  Height:  |  Size: 623 B

@@ -0,0 +1,10 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" />
<path d="M253.52 7.34c134.563.152 248.14 110.661 243.98 245.16l-245.064.823.064-.823V7.5" stroke="#000" />
<path d="M252.5 7.5v490" fill="#d8d8d8" stroke="#000" stroke-width="10" />
<path
d="M252.116 496.393c134.563-.152 248.14-110.661 243.98-245.16l-245.064-.823-243.084 4.564c-8.862-.406 19.049 230.995 243.148 241.259"
stroke="#000" />
<path d="M7.5 252.5h490" fill="#d8d8d8" stroke="#000" stroke-width="10" />
</svg>

After

Width:  |  Height:  |  Size: 676 B

@@ -0,0 +1,5 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
r="245" />
<path d="M252.5 7.5v490" fill="#d8d8d8" stroke="#fff" stroke-width="30" />
</svg>

After

Width:  |  Height:  |  Size: 304 B

@@ -0,0 +1,4 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
r="245" />
</svg>

After

Width:  |  Height:  |  Size: 227 B

@@ -0,0 +1,6 @@
<svg width="64" height="64" viewBox="0 0 49 49" xmlns="http://www.w3.org/2000/svg">
<g fill="none" stroke="#000" stroke-width=".972">
<path d="M7.578 7.698l33.245 33.245M40.823 7.698L7.578 40.943" />
</g>
<circle cx="24.071" cy="24.071" r="23.571" fill="none" stroke="#000" />
</svg>

After

Width:  |  Height:  |  Size: 298 B

@@ -0,0 +1,6 @@
<svg width="64" height="64" viewBox="0 0 505 505" xmlns="http://www.w3.org/2000/svg">
<circle style="transform-box:fill-box;transform-origin:50% 50%" cx="252.5" cy="252.5" stroke="#000" stroke-width="10"
fill="transparent" r="245" /><text style="white-space:pre"
transform="matrix(6.23986 0 0 6.03723 -1447.916 -1416.396)" x="259.868" y="296.998" fill="rgba(0,0,0,.27)"
font-family="Noto Sans" font-size="53" font-weight="700">?</text>
</svg>

After

Width:  |  Height:  |  Size: 456 B