[synopanalyzer] Add IconProvider (#20193)
* Add IconProvider Signed-off-by: gael@lhopital.org <gael@lhopital.org>
@@ -19,3 +19,11 @@ Based on the commit bed5842 / 3 May 2017
|
||||
* License: EPL + MIT License
|
||||
* Project: 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.
|
||||
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 |  |
|
||||
| oh:synopanalyzer:octa | Yes |  |
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
- `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 |
|
||||
| time-utc | DateTime | Observation time of the Synop message |
|
||||
| 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).
|
||||
|
||||
@@ -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" }
|
||||
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 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" }
|
||||
```
|
||||
|
||||
### 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
|
||||
public class SynopAnalyzerBindingConstants {
|
||||
private static final String BINDING_ID = "synopanalyzer";
|
||||
public static final String BINDING_ID = "synopanalyzer";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
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_SPEED_BEAUFORT = "wind-speed-beaufort";
|
||||
public static final String TIME_UTC = "time-utc";
|
||||
public static final String WIND_BARB = "wind-barb";
|
||||
|
||||
// Default units
|
||||
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'/>");
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ package org.openhab.binding.synopanalyzer.internal.handler;
|
||||
import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -23,6 +24,7 @@ import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
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.Nullable;
|
||||
import org.openhab.binding.synopanalyzer.internal.WindBarbGenerator;
|
||||
import org.openhab.binding.synopanalyzer.internal.config.SynopAnalyzerConfiguration;
|
||||
import org.openhab.binding.synopanalyzer.internal.stationdb.Station;
|
||||
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.PointType;
|
||||
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.unit.SIUnits;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.ChannelUID;
|
||||
import org.openhab.core.thing.Thing;
|
||||
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 int REQUEST_TIMEOUT_MS = 5000;
|
||||
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 OCTA_MAX = 8.0;
|
||||
|
||||
@@ -82,7 +88,7 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
|
||||
private final LocationProvider locationProvider;
|
||||
private final List<Station> stations;
|
||||
|
||||
private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
|
||||
private @Nullable ScheduledFuture<?> refreshJob;
|
||||
private @NonNullByDefault({}) String formattedStationId;
|
||||
|
||||
public SynopAnalyzerHandler(Thing thing, LocationProvider locationProvider, List<Station> stations) {
|
||||
@@ -104,8 +110,8 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
|
||||
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::updateChannels, 0,
|
||||
configuration.refreshInterval, TimeUnit.MINUTES));
|
||||
refreshJob = scheduler.scheduleWithFixedDelay(this::updateChannels, 0, configuration.refreshInterval,
|
||||
TimeUnit.MINUTES);
|
||||
}
|
||||
|
||||
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);
|
||||
List<String> messages = Arrays.asList(answer.split("\n"));
|
||||
if (!messages.isEmpty()) {
|
||||
String message = messages.get(messages.size() - 1);
|
||||
String message = messages.getLast();
|
||||
logger.debug(message);
|
||||
if (message.startsWith(formattedStationId)) {
|
||||
logger.debug("Valid Synop message received");
|
||||
|
||||
List<String> messageParts = Arrays.asList(message.split(","));
|
||||
String synopMessage = messageParts.get(messageParts.size() - 1);
|
||||
String synopMessage = messageParts.getLast();
|
||||
|
||||
return createSynopObject(synopMessage);
|
||||
}
|
||||
@@ -167,6 +173,7 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
|
||||
private State getChannelState(String channelId, Synop synop) {
|
||||
int octa = synop.getOcta();
|
||||
Integer direction = synop.getWindDirection();
|
||||
QuantityType<Speed> windSpeed = getWindStrength(synop);
|
||||
switch (channelId) {
|
||||
case HORIZONTAL_VISIBILITY:
|
||||
return new StringType(synop.getHorizontalVisibility().name());
|
||||
@@ -181,7 +188,7 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
|
||||
return UnDefType.NULL;
|
||||
case OVERCAST:
|
||||
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:
|
||||
return new QuantityType<>(synop.getPressure(), PRESSURE_UNIT);
|
||||
case TEMPERATURE:
|
||||
@@ -193,10 +200,16 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
|
||||
: UnDefType.NULL;
|
||||
case WIND_STRENGTH:
|
||||
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:
|
||||
QuantityType<Speed> wsKpH = getWindStrength(synop).toUnit(SIUnits.KILOMETRE_PER_HOUR);
|
||||
return wsKpH != null ? new DecimalType(Math.round(Math.pow(wsKpH.floatValue() / 3.01, 0.666666666)))
|
||||
: UnDefType.NULL;
|
||||
QuantityType<Speed> wsKpH = Objects.requireNonNull(windSpeed.toUnit(SIUnits.KILOMETRE_PER_HOUR));
|
||||
return new DecimalType(Math.round(Math.pow(wsKpH.floatValue() / 3.01, 0.666666666)));
|
||||
case TIME_UTC:
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
int year = synop.getYear() == 0 ? now.getYear() : synop.getYear();
|
||||
@@ -236,8 +249,10 @@ public class SynopAnalyzerHandler extends BaseThingHandler {
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
refreshJob.ifPresent(job -> job.cancel(true));
|
||||
refreshJob = Optional.empty();
|
||||
if (refreshJob instanceof ScheduledFuture job) {
|
||||
job.cancel(true);
|
||||
}
|
||||
refreshJob = null;
|
||||
}
|
||||
|
||||
@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.time-utc.label = Observation Time
|
||||
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.description = Wind direction
|
||||
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-speed-beaufort.label = Beaufort
|
||||
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="horizontal-visibility" typeId="horizontal-visibility"/>
|
||||
<channel id="time-utc" typeId="time-utc"/>
|
||||
<channel id="wind-barb" typeId="wind-barb"/>
|
||||
</channels>
|
||||
|
||||
<config-description>
|
||||
@@ -41,7 +42,7 @@
|
||||
<item-type>Number</item-type>
|
||||
<label>Beaufort</label>
|
||||
<description>Wind speed in Beaufort Scale</description>
|
||||
<category>Wind</category>
|
||||
<category>oh:synopanalyzer:beaufort</category>
|
||||
<tags>
|
||||
<tag>Calculation</tag>
|
||||
<tag>Speed</tag>
|
||||
@@ -84,7 +85,7 @@
|
||||
<item-type>Number</item-type>
|
||||
<label>Octa</label>
|
||||
<description>Cloud cover estimation.</description>
|
||||
<category>sun_clouds</category>
|
||||
<category>oh:synopanalyzer:octa</category>
|
||||
<state readOnly="true" pattern="%d/8" min="0" max="8"/>
|
||||
</channel-type>
|
||||
|
||||
@@ -93,6 +94,9 @@
|
||||
<label>Attenuation Factor</label>
|
||||
<description>Cloud layer attenuation factor</description>
|
||||
<category>sun_clouds</category>
|
||||
<tags>
|
||||
<tag>Calculation</tag>
|
||||
</tags>
|
||||
<state readOnly="true" pattern="%.1f" max="1" min="0"/>
|
||||
</channel-type>
|
||||
|
||||
@@ -129,6 +133,17 @@
|
||||
<label>Observation Time</label>
|
||||
<description>Timestamp when data was observed</description>
|
||||
<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"/>
|
||||
</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 |