added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
32
bundles/org.openhab.binding.fmiweather/.classpath
Normal file
32
bundles/org.openhab.binding.fmiweather/.classpath
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<classpath>
|
||||
<classpathentry kind="src" output="target/classes" path="src/main/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
|
||||
<attributes>
|
||||
<attribute name="optional" value="true"/>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
<attribute name="test" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
|
||||
<attributes>
|
||||
<attribute name="maven.pomderived" value="true"/>
|
||||
</attributes>
|
||||
</classpathentry>
|
||||
<classpathentry kind="output" path="target/classes"/>
|
||||
</classpath>
|
||||
23
bundles/org.openhab.binding.fmiweather/.project
Normal file
23
bundles/org.openhab.binding.fmiweather/.project
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>org.openhab.binding.fmiweather</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.jdt.core.javabuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
<buildCommand>
|
||||
<name>org.eclipse.m2e.core.maven2Builder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.eclipse.jdt.core.javanature</nature>
|
||||
<nature>org.eclipse.m2e.core.maven2Nature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
||||
15
bundles/org.openhab.binding.fmiweather/NOTICE
Normal file
15
bundles/org.openhab.binding.fmiweather/NOTICE
Normal file
@@ -0,0 +1,15 @@
|
||||
This content is produced and maintained by the openHAB project.
|
||||
|
||||
* Project home: https://www.openhab.org
|
||||
|
||||
== Declared Project Licenses
|
||||
|
||||
This program and the accompanying materials are made available under the terms
|
||||
of the Eclipse Public License 2.0 which is available at
|
||||
https://www.eclipse.org/legal/epl-2.0/.
|
||||
|
||||
== Source Code
|
||||
|
||||
https://github.com/openhab/openhab2-addons
|
||||
|
||||
|
||||
1470
bundles/org.openhab.binding.fmiweather/README.md
Normal file
1470
bundles/org.openhab.binding.fmiweather/README.md
Normal file
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
17
bundles/org.openhab.binding.fmiweather/pom.xml
Normal file
17
bundles/org.openhab.binding.fmiweather/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.openhab.addons.bundles</groupId>
|
||||
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
|
||||
<version>3.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>org.openhab.binding.fmiweather</artifactId>
|
||||
|
||||
<name>openHAB Add-ons :: Bundles :: fmiweather Binding</name>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.fmiweather-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
|
||||
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
|
||||
|
||||
<feature name="openhab-binding-fmiweather" description="fmiweather Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.fmiweather/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,274 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal;
|
||||
|
||||
import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.fmiweather.internal.client.Client;
|
||||
import org.openhab.binding.fmiweather.internal.client.Data;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMIResponse;
|
||||
import org.openhab.binding.fmiweather.internal.client.Request;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIResponseException;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIUnexpectedResponseException;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
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.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link AbstractWeatherHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public abstract class AbstractWeatherHandler extends BaseThingHandler {
|
||||
|
||||
private static final ZoneId UTC = ZoneId.of("UTC");
|
||||
protected static final String PROP_LONGITUDE = "longitude";
|
||||
protected static final String PROP_LATITUDE = "latitude";
|
||||
protected static final String PROP_NAME = "name";
|
||||
protected static final String PROP_REGION = "region";
|
||||
private static final long REFRESH_THROTTLE_MILLIS = 10_000;
|
||||
|
||||
protected static final int TIMEOUT_MILLIS = 10_000;
|
||||
private final Logger logger = LoggerFactory.getLogger(AbstractWeatherHandler.class);
|
||||
|
||||
protected volatile @NonNullByDefault({}) Client client;
|
||||
protected final AtomicReference<@Nullable ScheduledFuture<?>> futureRef = new AtomicReference<>();
|
||||
protected volatile @Nullable FMIResponse response;
|
||||
protected volatile int pollIntervalSeconds = 120; // reset by subclasses
|
||||
|
||||
private volatile long lastRefreshMillis = 0;
|
||||
private final AtomicReference<@Nullable ScheduledFuture<?>> updateChannelsFutureRef = new AtomicReference<>();
|
||||
|
||||
public AbstractWeatherHandler(Thing thing) {
|
||||
super(thing);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (RefreshType.REFRESH == command) {
|
||||
ScheduledFuture<?> prevFuture = updateChannelsFutureRef.get();
|
||||
ScheduledFuture<?> newFuture = updateChannelsFutureRef
|
||||
.updateAndGet(fut -> fut == null || fut.isDone() ? submitUpdateChannelsThrottled() : fut);
|
||||
assert newFuture != null; // invariant
|
||||
if (logger.isTraceEnabled()) {
|
||||
long delayRemainingMillis = newFuture.getDelay(TimeUnit.MILLISECONDS);
|
||||
if (delayRemainingMillis <= 0) {
|
||||
logger.trace("REFRESH received. Channels are updated");
|
||||
} else {
|
||||
logger.trace("REFRESH received. Delaying by {} ms to avoid throttle excessive REFRESH",
|
||||
delayRemainingMillis);
|
||||
}
|
||||
if (prevFuture == newFuture) {
|
||||
logger.trace("REFRESH received. Previous refresh ongoing, will wait for it to complete in {} ms",
|
||||
lastRefreshMillis + REFRESH_THROTTLE_MILLIS - System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
client = new Client();
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
rescheduleUpdate(0, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call updateChannels asynchronously, possibly in a delayed fashion to throttle updates. This protects against a
|
||||
* situation where many channels receive REFRESH command, e.g. when openHAB is requesting to update channels
|
||||
*
|
||||
* @return scheduled future
|
||||
*/
|
||||
private ScheduledFuture<?> submitUpdateChannelsThrottled() {
|
||||
long now = System.currentTimeMillis();
|
||||
long nextRefresh = lastRefreshMillis + REFRESH_THROTTLE_MILLIS;
|
||||
lastRefreshMillis = now;
|
||||
if (now > nextRefresh) {
|
||||
return scheduler.schedule(this::updateChannels, 0, TimeUnit.MILLISECONDS);
|
||||
} else {
|
||||
long delayMillis = nextRefresh - now;
|
||||
return scheduler.schedule(this::updateChannels, delayMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void updateChannels();
|
||||
|
||||
protected abstract Request getRequest();
|
||||
|
||||
protected void update(int retry) {
|
||||
if (retry < RETRIES) {
|
||||
try {
|
||||
response = client.query(getRequest(), TIMEOUT_MILLIS);
|
||||
} catch (FMIUnexpectedResponseException e) {
|
||||
handleError(e, retry);
|
||||
return;
|
||||
} catch (FMIResponseException e) {
|
||||
handleError(e, retry);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
logger.trace("Query failed. Retries exhausted, not trying again until next poll.");
|
||||
}
|
||||
// Update channel (if we have received a response)
|
||||
updateChannels();
|
||||
// Channels updated successfully or exhausted all retries. Reschedule new update
|
||||
rescheduleUpdate(pollIntervalSeconds * 1000, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
response = null;
|
||||
cancel(futureRef.getAndSet(null), true);
|
||||
cancel(updateChannelsFutureRef.getAndSet(null), true);
|
||||
}
|
||||
|
||||
protected static int lastValidIndex(Data data) {
|
||||
if (data.values.length < 2) {
|
||||
throw new IllegalStateException("Excepted at least two data items");
|
||||
}
|
||||
if (data.values[0] == null) {
|
||||
return -1;
|
||||
}
|
||||
for (int i = 1; i < data.values.length; i++) {
|
||||
if (data.values[i] == null) {
|
||||
return i - 1;
|
||||
}
|
||||
}
|
||||
if (data.values[data.values.length - 1] == null) {
|
||||
return -1;
|
||||
}
|
||||
return data.values.length - 1;
|
||||
}
|
||||
|
||||
protected static long floorToEvenMinutes(long epochSeconds, int roundMinutes) {
|
||||
long roundSecs = roundMinutes * 60;
|
||||
return (epochSeconds / roundSecs) * roundSecs;
|
||||
}
|
||||
|
||||
protected static long ceilToEvenMinutes(long epochSeconds, int roundMinutes) {
|
||||
double epochDouble = epochSeconds;
|
||||
long roundSecs = roundMinutes * 60;
|
||||
double roundSecsDouble = (roundMinutes * 60);
|
||||
return (long) Math.ceil(epochDouble / roundSecsDouble) * roundSecs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update QuantityType channel state
|
||||
*
|
||||
* @param channelUID channel UID
|
||||
* @param epochSecond value to update
|
||||
* @param unit unit associated with the value
|
||||
*/
|
||||
protected <T extends Quantity<T>> void updateEpochSecondStateIfLinked(ChannelUID channelUID, long epochSecond) {
|
||||
if (isLinked(channelUID)) {
|
||||
updateState(channelUID, new DateTimeType(ZonedDateTime.ofInstant(Instant.ofEpochSecond(epochSecond), UTC)
|
||||
.withZoneSameInstant(ZoneId.systemDefault())));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update QuantityType or DecimalType channel state
|
||||
*
|
||||
* Updates UNDEF state when value is null
|
||||
*
|
||||
* @param channelUID channel UID
|
||||
* @param value value to update
|
||||
* @param unit unit associated with the value
|
||||
*/
|
||||
protected void updateStateIfLinked(ChannelUID channelUID, @Nullable BigDecimal value, @Nullable Unit<?> unit) {
|
||||
if (isLinked(channelUID)) {
|
||||
if (value == null) {
|
||||
updateState(channelUID, UnDefType.UNDEF);
|
||||
} else if (unit == null) {
|
||||
updateState(channelUID, new DecimalType(value));
|
||||
} else {
|
||||
updateState(channelUID, new QuantityType<>(value, unit));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unwrap optional value and log with ERROR if value is not present
|
||||
*
|
||||
* This should be used only when we expect value to be present, and the reason for missing value corresponds to
|
||||
* description of {@link FMIUnexpectedResponseException}.
|
||||
*
|
||||
* @param optional optional to unwrap
|
||||
* @param messageIfNotPresent logging message
|
||||
* @param args arguments to logging
|
||||
* @throws FMIUnexpectedResponseException when value is not present
|
||||
* @return unwrapped value of the optional
|
||||
*/
|
||||
protected <T> T unwrap(Optional<T> optional, String messageIfNotPresent, Object... args)
|
||||
throws FMIUnexpectedResponseException {
|
||||
if (optional.isPresent()) {
|
||||
return optional.get();
|
||||
} else {
|
||||
// logger.error(messageIfNotPresent, args) avoided due to static analyzer
|
||||
String formattedMessage = String.format(messageIfNotPresent, args);
|
||||
throw new FMIUnexpectedResponseException(formattedMessage);
|
||||
}
|
||||
}
|
||||
|
||||
protected void handleError(FMIResponseException e, int retry) {
|
||||
response = null;
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
String.format("%s: %s", e.getClass().getSimpleName(), e.getMessage()));
|
||||
logger.trace("Query failed. Increase retry count {} and try again. Error: {} {}", retry, e.getClass().getName(),
|
||||
e.getMessage());
|
||||
// Try again, with increased retry count
|
||||
rescheduleUpdate(RETRY_DELAY_MILLIS, false, retry + 1);
|
||||
}
|
||||
|
||||
protected void rescheduleUpdate(long delayMillis, boolean mayInterruptIfRunning) {
|
||||
rescheduleUpdate(delayMillis, mayInterruptIfRunning, 0);
|
||||
}
|
||||
|
||||
protected void rescheduleUpdate(long delayMillis, boolean mayInterruptIfRunning, int retry) {
|
||||
cancel(futureRef.getAndSet(scheduler.schedule(() -> this.update(retry), delayMillis, TimeUnit.MILLISECONDS)),
|
||||
mayInterruptIfRunning);
|
||||
}
|
||||
|
||||
private static void cancel(@Nullable ScheduledFuture<?> future, boolean mayInterruptIfRunning) {
|
||||
if (future != null) {
|
||||
future.cancel(mayInterruptIfRunning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
|
||||
/**
|
||||
* The {@link BindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class BindingConstants {
|
||||
|
||||
private static final String BINDING_ID = "fmiweather";
|
||||
|
||||
public static final int RETRIES = 3;
|
||||
public static final int RETRY_DELAY_MILLIS = 1500;
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_OBSERVATION = new ThingTypeUID(BINDING_ID, "observation");
|
||||
public static final ThingTypeUID THING_TYPE_FORECAST = new ThingTypeUID(BINDING_ID, "forecast");
|
||||
public static final ThingUID UID_LOCAL_FORECAST = new ThingUID(BINDING_ID, "forecast", "local");
|
||||
|
||||
// List of all Channel ids
|
||||
public static final String CHANNEL_TIME = "time";
|
||||
public static final String CHANNEL_TEMPERATURE = "temperature";
|
||||
public static final String CHANNEL_HUMIDITY = "humidity";
|
||||
public static final String CHANNEL_WIND_DIRECTION = "wind-direction";
|
||||
public static final String CHANNEL_WIND_SPEED = "wind-speed";
|
||||
public static final String CHANNEL_GUST = "wind-gust";
|
||||
public static final String CHANNEL_PRESSURE = "pressure";
|
||||
public static final String CHANNEL_PRECIPITATION_AMOUNT = "precipitation";
|
||||
public static final String CHANNEL_SNOW_DEPTH = "snow-depth";
|
||||
public static final String CHANNEL_VISIBILITY = "visibility";
|
||||
public static final String CHANNEL_CLOUDS = "clouds";
|
||||
public static final String CHANNEL_OBSERVATION_PRESENT_WEATHER = "present-weather";
|
||||
|
||||
public static final String CHANNEL_TOTAL_CLOUD_COVER = "total-cloud-cover";
|
||||
public static final String CHANNEL_PRECIPITATION_INTENSITY = "precipitation-intensity";
|
||||
public static final String CHANNEL_FORECAST_WEATHER_ID = "weather-id";
|
||||
|
||||
// Configuration properties
|
||||
public static final String FMISID = "fmisid";
|
||||
public static final String LOCATION = "location";
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal;
|
||||
|
||||
import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
|
||||
import static org.openhab.binding.fmiweather.internal.client.ForecastRequest.*;
|
||||
import static org.openhab.core.library.unit.SIUnits.CELSIUS;
|
||||
import static org.openhab.core.library.unit.SmartHomeUnits.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.measure.Unit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.fmiweather.internal.client.Data;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMIResponse;
|
||||
import org.openhab.binding.fmiweather.internal.client.ForecastRequest;
|
||||
import org.openhab.binding.fmiweather.internal.client.LatLon;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
import org.openhab.binding.fmiweather.internal.client.Request;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIUnexpectedResponseException;
|
||||
import org.openhab.core.thing.Channel;
|
||||
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.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ForecastWeatherHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ForecastWeatherHandler extends AbstractWeatherHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ForecastWeatherHandler.class);
|
||||
|
||||
private static final String GROUP_FORECAST_NOW = "forecastNow";
|
||||
private static final int QUERY_RESOLUTION_MINUTES = 20; // The channel group hours should be divisible by this
|
||||
// Hirlam horizon is 54h https://ilmatieteenlaitos.fi/avoin-data-avattavat-aineistot (in Finnish)
|
||||
private static final int FORECAST_HORIZON_HOURS = 50; // should be divisible by QUERY_RESOLUTION_MINUTES
|
||||
private static final Map<String, Map.Entry<String, @Nullable Unit<?>>> CHANNEL_TO_FORECAST_FIELD_NAME_AND_UNIT = new HashMap<>(
|
||||
9);
|
||||
|
||||
private static void addMapping(String channelId, String requestField, @Nullable Unit<?> unit) {
|
||||
CHANNEL_TO_FORECAST_FIELD_NAME_AND_UNIT.put(channelId,
|
||||
new AbstractMap.SimpleImmutableEntry<>(requestField, unit));
|
||||
}
|
||||
|
||||
static {
|
||||
addMapping(CHANNEL_TEMPERATURE, PARAM_TEMPERATURE, CELSIUS);
|
||||
addMapping(CHANNEL_HUMIDITY, PARAM_HUMIDITY, PERCENT);
|
||||
addMapping(CHANNEL_WIND_DIRECTION, PARAM_WIND_DIRECTION, DEGREE_ANGLE);
|
||||
addMapping(CHANNEL_WIND_SPEED, PARAM_WIND_SPEED, METRE_PER_SECOND);
|
||||
addMapping(CHANNEL_GUST, PARAM_WIND_GUST, METRE_PER_SECOND);
|
||||
addMapping(CHANNEL_PRESSURE, PARAM_PRESSURE, MILLIBAR);
|
||||
addMapping(CHANNEL_PRECIPITATION_INTENSITY, PARAM_PRECIPITATION_1H, MILLIMETRE_PER_HOUR);
|
||||
addMapping(CHANNEL_TOTAL_CLOUD_COVER, PARAM_TOTAL_CLOUD_COVER, PERCENT);
|
||||
addMapping(CHANNEL_FORECAST_WEATHER_ID, PARAM_WEATHER_SYMBOL, null);
|
||||
}
|
||||
|
||||
private @NonNullByDefault({}) LatLon location;
|
||||
|
||||
public ForecastWeatherHandler(Thing thing) {
|
||||
super(thing);
|
||||
// Override poll interval to slower value
|
||||
pollIntervalSeconds = (int) TimeUnit.MINUTES.toSeconds(QUERY_RESOLUTION_MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
try {
|
||||
Object location = getConfig().get(BindingConstants.LOCATION);
|
||||
if (location == null) {
|
||||
logger.debug("Location not set for thing {} -- aborting initialization.", getThing().getUID());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
String.format("location parameter not set"));
|
||||
return;
|
||||
}
|
||||
String latlon = location.toString();
|
||||
String[] split = latlon.split(",");
|
||||
if (split.length != 2) {
|
||||
throw new NumberFormatException(String.format(
|
||||
"Expecting location parameter to have latitude and longitude separated by comma (LATITUDE,LONGITUDE). Found %d values instead.",
|
||||
split.length));
|
||||
}
|
||||
this.location = new LatLon(new BigDecimal(split[0].trim()), new BigDecimal(split[1].trim()));
|
||||
super.initialize();
|
||||
} catch (NumberFormatException e) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, String.format(
|
||||
"location parameter should be in format LATITUDE,LONGITUDE. Error details: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
super.dispose();
|
||||
this.location = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request getRequest() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
return new ForecastRequest(location, floorToEvenMinutes(now, QUERY_RESOLUTION_MINUTES),
|
||||
ceilToEvenMinutes(now + TimeUnit.HOURS.toSeconds(FORECAST_HORIZON_HOURS), QUERY_RESOLUTION_MINUTES),
|
||||
QUERY_RESOLUTION_MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChannels() {
|
||||
FMIResponse response = this.response;
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Location location = unwrap(response.getLocations().stream().findFirst(),
|
||||
"No locations in response -- no data? Aborting");
|
||||
Map<String, String> properties = editProperties();
|
||||
properties.put(PROP_NAME, location.name);
|
||||
properties.put(PROP_LATITUDE, location.latitude.toPlainString());
|
||||
properties.put(PROP_LONGITUDE, location.longitude.toPlainString());
|
||||
updateProperties(properties);
|
||||
for (Channel channel : getThing().getChannels()) {
|
||||
ChannelUID channelUID = channel.getUID();
|
||||
int hours = getHours(channelUID);
|
||||
int timeIndex = getTimeIndex(hours);
|
||||
if (channelUID.getIdWithoutGroup().equals(CHANNEL_TIME)) {
|
||||
// All parameters and locations should share the same timestamps. We use temperature to figure out
|
||||
// timestamp for the group of channels
|
||||
String field = ForecastRequest.PARAM_TEMPERATURE;
|
||||
Data data = unwrap(response.getData(location, field),
|
||||
"Field %s not present for location %s in response. Bug?", field, location);
|
||||
updateEpochSecondStateIfLinked(channelUID, data.timestampsEpochSecs[timeIndex]);
|
||||
} else {
|
||||
String field = getDataField(channelUID);
|
||||
Unit<?> unit = getUnit(channelUID);
|
||||
if (field == null) {
|
||||
logger.error("Channel {} not handled. Bug?", channelUID.getId());
|
||||
continue;
|
||||
}
|
||||
Data data = unwrap(response.getData(location, field),
|
||||
"Field %s not present for location %s in response. Bug?", field, location);
|
||||
updateStateIfLinked(channelUID, data.values[timeIndex], unit);
|
||||
}
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (FMIUnexpectedResponseException e) {
|
||||
// Unexpected (possibly bug) issue with response
|
||||
logger.warn("Unexpected response encountered: {}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
String.format("Unexpected API response: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private static int getHours(ChannelUID uid) {
|
||||
String groupId = uid.getGroupId();
|
||||
if (groupId == null) {
|
||||
throw new IllegalStateException("All channels should be in group!");
|
||||
}
|
||||
if (GROUP_FORECAST_NOW.equals(groupId)) {
|
||||
return 0;
|
||||
} else {
|
||||
return Integer.valueOf(groupId.substring(groupId.length() - 2));
|
||||
}
|
||||
}
|
||||
|
||||
private static int getTimeIndex(int hours) {
|
||||
return (int) (TimeUnit.HOURS.toMinutes(hours) / QUERY_RESOLUTION_MINUTES);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unused", "null" })
|
||||
private static @Nullable String getDataField(ChannelUID channelUID) {
|
||||
Entry<String, @Nullable Unit<?>> entry = CHANNEL_TO_FORECAST_FIELD_NAME_AND_UNIT
|
||||
.get(channelUID.getIdWithoutGroup());
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
return entry.getKey();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unused", "null" })
|
||||
private static @Nullable Unit<?> getUnit(ChannelUID channelUID) {
|
||||
Entry<String, @Nullable Unit<?>> entry = CHANNEL_TO_FORECAST_FIELD_NAME_AND_UNIT
|
||||
.get(channelUID.getIdWithoutGroup());
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal;
|
||||
|
||||
import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
||||
import org.openhab.core.thing.binding.ThingHandler;
|
||||
import org.openhab.core.thing.binding.ThingHandlerFactory;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* The {@link HandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(configurationPid = "binding.fmiweather", service = ThingHandlerFactory.class)
|
||||
public class HandlerFactory extends BaseThingHandlerFactory {
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = new HashSet<>(
|
||||
Arrays.asList(THING_TYPE_OBSERVATION, THING_TYPE_FORECAST));
|
||||
|
||||
@Override
|
||||
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
|
||||
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable ThingHandler createHandler(Thing thing) {
|
||||
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||
|
||||
if (THING_TYPE_OBSERVATION.equals(thingTypeUID)) {
|
||||
return new ObservationWeatherHandler(thing);
|
||||
} else if (THING_TYPE_FORECAST.equals(thingTypeUID)) {
|
||||
return new ForecastWeatherHandler(thing);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal;
|
||||
|
||||
import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
|
||||
import static org.openhab.binding.fmiweather.internal.client.ObservationRequest.*;
|
||||
import static org.openhab.core.library.unit.SIUnits.*;
|
||||
import static org.openhab.core.library.unit.SmartHomeUnits.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Length;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.fmiweather.internal.client.Data;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMIResponse;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMISID;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
import org.openhab.binding.fmiweather.internal.client.ObservationRequest;
|
||||
import org.openhab.binding.fmiweather.internal.client.Request;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIUnexpectedResponseException;
|
||||
import org.openhab.core.library.unit.MetricPrefix;
|
||||
import org.openhab.core.thing.Channel;
|
||||
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.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link ObservationWeatherHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ObservationWeatherHandler extends AbstractWeatherHandler {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ObservationWeatherHandler.class);
|
||||
private static final long OBSERVATION_LOOK_BACK_SECONDS = TimeUnit.MINUTES.toSeconds(30);
|
||||
private static final int STEP_MINUTES = 10;
|
||||
private static final int POLL_INTERVAL_SECONDS = 600;
|
||||
private static BigDecimal HUNDRED = BigDecimal.valueOf(100);
|
||||
private static BigDecimal NA_CLOUD_MAX = BigDecimal.valueOf(8); // API value when having full clouds (overcast)
|
||||
private static BigDecimal NA_CLOUD_COVERAGE = BigDecimal.valueOf(9); // API value when cloud coverage could not be
|
||||
// determined.
|
||||
|
||||
public static final Unit<Length> MILLIMETRE = MetricPrefix.MILLI(METRE);
|
||||
public static final Unit<Length> CENTIMETRE = MetricPrefix.CENTI(METRE);
|
||||
|
||||
private static final Map<String, Map.Entry<String, @Nullable Unit<?>>> CHANNEL_TO_OBSERVATION_FIELD_NAME_AND_UNIT = new HashMap<>(
|
||||
11);
|
||||
private static final Map<String, @Nullable Function<BigDecimal, @Nullable BigDecimal>> OBSERVATION_FIELD_NAME_TO_CONVERSION_FUNC = new HashMap<>(
|
||||
11);
|
||||
|
||||
private static void addMapping(String channelId, String requestField, @Nullable Unit<?> result_unit,
|
||||
@Nullable Function<BigDecimal, @Nullable BigDecimal> conversion) {
|
||||
CHANNEL_TO_OBSERVATION_FIELD_NAME_AND_UNIT.put(channelId,
|
||||
new AbstractMap.SimpleImmutableEntry<>(requestField, result_unit));
|
||||
OBSERVATION_FIELD_NAME_TO_CONVERSION_FUNC.put(requestField, conversion);
|
||||
}
|
||||
|
||||
static {
|
||||
addMapping(CHANNEL_TEMPERATURE, PARAM_TEMPERATURE, CELSIUS, null);
|
||||
addMapping(CHANNEL_HUMIDITY, PARAM_HUMIDITY, PERCENT, null);
|
||||
addMapping(CHANNEL_WIND_DIRECTION, PARAM_WIND_DIRECTION, DEGREE_ANGLE, null);
|
||||
addMapping(CHANNEL_WIND_SPEED, PARAM_WIND_SPEED, METRE_PER_SECOND, null);
|
||||
addMapping(CHANNEL_GUST, PARAM_WIND_GUST, METRE_PER_SECOND, null);
|
||||
addMapping(CHANNEL_PRESSURE, PARAM_PRESSURE, MILLIBAR, null);
|
||||
addMapping(CHANNEL_PRECIPITATION_AMOUNT, PARAM_PRECIPITATION_AMOUNT, MILLIMETRE, null);
|
||||
addMapping(CHANNEL_SNOW_DEPTH, PARAM_SNOW_DEPTH, CENTIMETRE, null);
|
||||
addMapping(CHANNEL_VISIBILITY, PARAM_VISIBILITY, METRE, null);
|
||||
// Converting 0...8 scale to percentage 0...100%. Value of 9 is converted to null/UNDEF
|
||||
addMapping(CHANNEL_CLOUDS, PARAM_CLOUDS, PERCENT, clouds -> clouds.compareTo(NA_CLOUD_COVERAGE) == 0 ? null
|
||||
: clouds.divide(NA_CLOUD_MAX).multiply(HUNDRED));
|
||||
addMapping(CHANNEL_OBSERVATION_PRESENT_WEATHER, PARAM_PRESENT_WEATHER, null, null);
|
||||
}
|
||||
|
||||
private @NonNullByDefault({}) String fmisid;
|
||||
|
||||
public ObservationWeatherHandler(Thing thing) {
|
||||
super(thing);
|
||||
pollIntervalSeconds = POLL_INTERVAL_SECONDS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
fmisid = Objects.toString(getConfig().get(BindingConstants.FMISID), null);
|
||||
if (fmisid == null) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
String.format("%s parameter not set", FMISID));
|
||||
} else {
|
||||
super.initialize();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Request getRequest() {
|
||||
long now = Instant.now().getEpochSecond();
|
||||
return new ObservationRequest(new FMISID(fmisid),
|
||||
floorToEvenMinutes(now - OBSERVATION_LOOK_BACK_SECONDS, STEP_MINUTES),
|
||||
ceilToEvenMinutes(now, STEP_MINUTES), STEP_MINUTES);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateChannels() {
|
||||
FMIResponse response = this.response;
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Location location = unwrap(response.getLocations().stream().findFirst(),
|
||||
"No locations in response -- no data? Aborting");
|
||||
Map<String, String> properties = editProperties();
|
||||
properties.put(PROP_NAME, location.name);
|
||||
properties.put(PROP_LATITUDE, location.latitude.toPlainString());
|
||||
properties.put(PROP_LONGITUDE, location.longitude.toPlainString());
|
||||
updateProperties(properties);
|
||||
// All parameters and locations should share the same timestamps. We use temperature to figure out most
|
||||
// recent timestamp which has non-NaN value
|
||||
int lastValidIndex = unwrap(
|
||||
response.getData(location, ObservationRequest.PARAM_TEMPERATURE).map(data -> lastValidIndex(data)),
|
||||
"lastValidIndex not available. Bug?");
|
||||
for (Channel channel : getThing().getChannels()) {
|
||||
ChannelUID channelUID = channel.getUID();
|
||||
if (lastValidIndex < 0) {
|
||||
updateState(channelUID, UnDefType.UNDEF);
|
||||
} else if (channelUID.getIdWithoutGroup().equals(CHANNEL_TIME)) {
|
||||
String field = ObservationRequest.PARAM_TEMPERATURE;
|
||||
Data data = unwrap(response.getData(location, field),
|
||||
"Field %s not present for location %s in response. Bug?", field, location);
|
||||
updateEpochSecondStateIfLinked(channelUID, data.timestampsEpochSecs[lastValidIndex]);
|
||||
} else {
|
||||
String field = getDataField(channelUID);
|
||||
Unit<?> unit = getUnit(channelUID);
|
||||
if (field == null) {
|
||||
logger.error("Channel {} not handled. Bug?", channelUID.getId());
|
||||
continue;
|
||||
}
|
||||
Data data = unwrap(response.getData(location, field),
|
||||
"Field %s not present for location % in response. Bug?", field, location);
|
||||
BigDecimal rawValue = data.values[lastValidIndex];
|
||||
BigDecimal processedValue = preprocess(field, rawValue);
|
||||
updateStateIfLinked(channelUID, processedValue, unit);
|
||||
}
|
||||
}
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
} catch (FMIUnexpectedResponseException e) {
|
||||
// Unexpected (possibly bug) issue with response
|
||||
logger.warn("Unexpected response encountered: {}", e.getMessage());
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
String.format("Unexpected API response: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
private static @Nullable String getDataField(ChannelUID channelUID) {
|
||||
Entry<String, @Nullable Unit<?>> entry = CHANNEL_TO_OBSERVATION_FIELD_NAME_AND_UNIT
|
||||
.get(channelUID.getIdWithoutGroup());
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
return entry.getKey();
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "null", "unused" })
|
||||
private static @Nullable Unit<?> getUnit(ChannelUID channelUID) {
|
||||
Entry<String, @Nullable Unit<?>> entry = CHANNEL_TO_OBSERVATION_FIELD_NAME_AND_UNIT
|
||||
.get(channelUID.getIdWithoutGroup());
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
return entry.getValue();
|
||||
}
|
||||
|
||||
private static @Nullable BigDecimal preprocess(String fieldName, @Nullable BigDecimal rawValue) {
|
||||
if (rawValue == null) {
|
||||
return null;
|
||||
}
|
||||
Function<BigDecimal, @Nullable BigDecimal> func = OBSERVATION_FIELD_NAME_TO_CONVERSION_FUNC.get(fieldName);
|
||||
if (func == null) {
|
||||
// No conversion required
|
||||
return rawValue;
|
||||
}
|
||||
return func.apply(rawValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import javax.xml.namespace.NamespaceContext;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
import javax.xml.xpath.XPath;
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpression;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMIResponse.Builder;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIExceptionReportException;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIIOException;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIUnexpectedResponseException;
|
||||
import org.openhab.core.io.net.http.HttpUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
*
|
||||
* Client for accessing FMI weather data
|
||||
*
|
||||
* Subject to license terms https://en.ilmatieteenlaitos.fi/open-data
|
||||
*
|
||||
*
|
||||
* All weather stations:
|
||||
* https://opendata.fmi.fi/wfs/fin?service=WFS&version=2.0.0&request=GetFeature&storedquery_id=fmi::ef::stations&networkid=121&
|
||||
* Networkid parameter isexplained in entries of
|
||||
* https://opendata.fmi.fi/wfs/fin?service=WFS&version=2.0.0&request=GetFeature&storedquery_id=fmi::ef::stations
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Client {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(Client.class);
|
||||
|
||||
public static final String WEATHER_STATIONS_URL = "https://opendata.fmi.fi/wfs/fin?service=WFS&version=2.0.0&request=GetFeature&storedquery_id=fmi::ef::stations&networkid=121&";
|
||||
|
||||
private static final Map<String, String> NAMESPACES = new HashMap<>();
|
||||
static {
|
||||
NAMESPACES.put("target", "http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0");
|
||||
NAMESPACES.put("gml", "http://www.opengis.net/gml/3.2");
|
||||
NAMESPACES.put("xlink", "http://www.w3.org/1999/xlink");
|
||||
NAMESPACES.put("ows", "http://www.opengis.net/ows/1.1");
|
||||
NAMESPACES.put("gmlcov", "http://www.opengis.net/gmlcov/1.0");
|
||||
NAMESPACES.put("swe", "http://www.opengis.net/swe/2.0");
|
||||
|
||||
NAMESPACES.put("wfs", "http://www.opengis.net/wfs/2.0");
|
||||
NAMESPACES.put("ef", "http://inspire.ec.europa.eu/schemas/ef/4.0");
|
||||
}
|
||||
private static final NamespaceContext NAMESPACE_CONTEXT = new NamespaceContext() {
|
||||
@Override
|
||||
public String getNamespaceURI(@Nullable String prefix) {
|
||||
return NAMESPACES.get(prefix);
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public @Nullable Iterator getPrefixes(@Nullable String val) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getPrefix(@Nullable String uri) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
private DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
|
||||
private DocumentBuilder documentBuilder;
|
||||
|
||||
public Client() {
|
||||
documentBuilderFactory.setNamespaceAware(true);
|
||||
try {
|
||||
documentBuilder = documentBuilderFactory.newDocumentBuilder();
|
||||
} catch (ParserConfigurationException e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query request and return the data
|
||||
*
|
||||
* @param request request to process
|
||||
* @param timeoutMillis timeout for the http call
|
||||
* @return data corresponding to the query
|
||||
* @throws FMIIOException on all I/O errors
|
||||
* @throws FMIUnexpectedResponseException on all unexpected content errors
|
||||
* @throw FMIExceptionReportException on explicit error responses from the server
|
||||
*/
|
||||
public FMIResponse query(Request request, int timeoutMillis)
|
||||
throws FMIExceptionReportException, FMIUnexpectedResponseException, FMIIOException {
|
||||
try {
|
||||
String url = request.toUrl();
|
||||
String responseText = HttpUtil.executeUrl("GET", url, timeoutMillis);
|
||||
if (responseText == null) {
|
||||
throw new FMIIOException(String.format("HTTP error with %s", request.toUrl()));
|
||||
}
|
||||
FMIResponse response = parseMultiPointCoverageXml(responseText);
|
||||
logger.debug("Request {} translated to url {}. Response: {}", request, url, response);
|
||||
return response;
|
||||
} catch (IOException e) {
|
||||
throw new FMIIOException(e);
|
||||
} catch (SAXException | XPathExpressionException e) {
|
||||
throw new FMIUnexpectedResponseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Query all weather stations
|
||||
*
|
||||
* @param timeoutMillis timeout for the http call
|
||||
* @return locations representing stations
|
||||
* @throws FMIIOException on all I/O errors
|
||||
* @throws FMIUnexpectedResponseException on all unexpected content errors
|
||||
* @throw FMIExceptionReportException on explicit error responses from the server
|
||||
*/
|
||||
public Set<Location> queryWeatherStations(int timeoutMillis)
|
||||
throws FMIIOException, FMIUnexpectedResponseException, FMIExceptionReportException {
|
||||
try {
|
||||
String response = HttpUtil.executeUrl("GET", WEATHER_STATIONS_URL, timeoutMillis);
|
||||
if (response == null) {
|
||||
throw new FMIIOException(String.format("HTTP error with %s", WEATHER_STATIONS_URL));
|
||||
}
|
||||
return parseStations(response);
|
||||
} catch (IOException e) {
|
||||
throw new FMIIOException(e);
|
||||
} catch (XPathExpressionException | SAXException e) {
|
||||
throw new FMIUnexpectedResponseException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Location> parseStations(String response) throws FMIExceptionReportException,
|
||||
FMIUnexpectedResponseException, SAXException, IOException, XPathExpressionException {
|
||||
Document document = documentBuilder.parse(new InputSource(new StringReader(response)));
|
||||
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
xPath.setNamespaceContext(NAMESPACE_CONTEXT);
|
||||
|
||||
boolean isExceptionReport = ((Node) xPath.compile("/ows:ExceptionReport").evaluate(document,
|
||||
XPathConstants.NODE)) != null;
|
||||
if (isExceptionReport) {
|
||||
Node exceptionCode = (Node) xPath.compile("/ows:ExceptionReport/ows:Exception/@exceptionCode")
|
||||
.evaluate(document, XPathConstants.NODE);
|
||||
String[] exceptionText = queryNodeValues(xPath.compile("//ows:ExceptionText/text()"), document);
|
||||
throw new FMIExceptionReportException(exceptionCode.getNodeValue(), exceptionText);
|
||||
}
|
||||
|
||||
String[] fmisids = queryNodeValues(
|
||||
xPath.compile(
|
||||
"/wfs:FeatureCollection/wfs:member/ef:EnvironmentalMonitoringFacility/gml:identifier/text()"),
|
||||
document);
|
||||
String[] names = queryNodeValues(xPath.compile(
|
||||
"/wfs:FeatureCollection/wfs:member/ef:EnvironmentalMonitoringFacility/gml:name[@codeSpace='http://xml.fmi.fi/namespace/locationcode/name']/text()"),
|
||||
document);
|
||||
String[] representativePoints = queryNodeValues(xPath.compile(
|
||||
"/wfs:FeatureCollection/wfs:member/ef:EnvironmentalMonitoringFacility/ef:representativePoint/gml:Point/gml:pos/text()"),
|
||||
document);
|
||||
|
||||
if (fmisids.length != names.length || fmisids.length != representativePoints.length) {
|
||||
throw new FMIUnexpectedResponseException(String.format(
|
||||
"Could not all properties of locations: fmisids: %d, names: %d, representativePoints: %d",
|
||||
fmisids.length, names.length, representativePoints.length));
|
||||
}
|
||||
|
||||
Set<Location> locations = new HashSet<>(representativePoints.length);
|
||||
for (int i = 0; i < representativePoints.length; i++) {
|
||||
BigDecimal[] latlon = parseLatLon(representativePoints[i]);
|
||||
locations.add(new Location(names[i], fmisids[i], latlon[0], latlon[1]));
|
||||
}
|
||||
return locations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse FMI multipointcoverage formatted xml response
|
||||
*
|
||||
*/
|
||||
private FMIResponse parseMultiPointCoverageXml(String response) throws FMIUnexpectedResponseException,
|
||||
FMIExceptionReportException, SAXException, IOException, XPathExpressionException {
|
||||
Document document = documentBuilder.parse(new InputSource(new StringReader(response)));
|
||||
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
xPath.setNamespaceContext(NAMESPACE_CONTEXT);
|
||||
|
||||
boolean isExceptionReport = ((Node) xPath.compile("/ows:ExceptionReport").evaluate(document,
|
||||
XPathConstants.NODE)) != null;
|
||||
if (isExceptionReport) {
|
||||
Node exceptionCode = (Node) xPath.compile("/ows:ExceptionReport/ows:Exception/@exceptionCode")
|
||||
.evaluate(document, XPathConstants.NODE);
|
||||
String[] exceptionText = queryNodeValues(xPath.compile("//ows:ExceptionText/text()"), document);
|
||||
throw new FMIExceptionReportException(exceptionCode.getNodeValue(), exceptionText);
|
||||
}
|
||||
|
||||
Builder builder = new FMIResponse.Builder();
|
||||
|
||||
String[] parameters = queryNodeValues(xPath.compile("//swe:field/@name"), document);
|
||||
/**
|
||||
* Observations have FMISID (FMI Station ID?), with forecasts we use lat & lon
|
||||
*/
|
||||
String[] ids = queryNodeValues(xPath.compile(
|
||||
"//target:Location/gml:identifier[@codeSpace='http://xml.fmi.fi/namespace/stationcode/fmisid']/text()"),
|
||||
document);
|
||||
|
||||
String[] names = queryNodeValues(xPath.compile(
|
||||
"//target:Location/gml:name[@codeSpace='http://xml.fmi.fi/namespace/locationcode/name']/text()"),
|
||||
document);
|
||||
String[] representativePointRefs = queryNodeValues(
|
||||
xPath.compile("//target:Location/target:representativePoint/@xlink:href"), document);
|
||||
|
||||
if ((ids.length > 0 && ids.length != names.length) || names.length != representativePointRefs.length) {
|
||||
throw new FMIUnexpectedResponseException(String.format(
|
||||
"Could not all properties of locations: ids: %d, names: %d, representativePointRefs: %d",
|
||||
ids.length, names.length, representativePointRefs.length));
|
||||
}
|
||||
|
||||
Location[] locations = new Location[representativePointRefs.length];
|
||||
for (int i = 0; i < locations.length; i++) {
|
||||
BigDecimal[] latlon = findLatLon(xPath, i, document, representativePointRefs[i]);
|
||||
String id = ids.length == 0 ? String.format("%s,%s", latlon[0].toPlainString(), latlon[1].toPlainString())
|
||||
: ids[i];
|
||||
locations[i] = new Location(names[i], id, latlon[0], latlon[1]);
|
||||
}
|
||||
|
||||
logger.trace("names ({}): {}", names.length, names);
|
||||
logger.trace("parameters ({}): {}", parameters.length, parameters);
|
||||
if (names.length == 0) {
|
||||
// No data, e.g. when starttime=endtime
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
String latLonTimeTripletText = takeFirstOrError("positions",
|
||||
queryNodeValues(xPath.compile("//gmlcov:positions/text()"), document));
|
||||
String[] latLonTimeTripletEntries = latLonTimeTripletText.trim().split("\\s+");
|
||||
logger.trace("latLonTimeTripletText: {}", latLonTimeTripletText);
|
||||
logger.trace("latLonTimeTripletEntries ({}): {}", latLonTimeTripletEntries.length, latLonTimeTripletEntries);
|
||||
int countTimestamps = latLonTimeTripletEntries.length / 3 / locations.length;
|
||||
long[] timestampsEpoch = IntStream.range(0, latLonTimeTripletEntries.length).filter(i -> i % 3 == 0)
|
||||
.limit(countTimestamps).mapToLong(i -> Long.parseLong(latLonTimeTripletEntries[i + 2])).toArray();
|
||||
// Invariant
|
||||
assert countTimestamps == timestampsEpoch.length;
|
||||
logger.trace("countTimestamps ({}): {}", countTimestamps, timestampsEpoch);
|
||||
validatePositionEntries(locations, timestampsEpoch, latLonTimeTripletEntries);
|
||||
|
||||
String valuesText = takeFirstOrError("doubleOrNilReasonTupleList",
|
||||
queryNodeValues(xPath.compile(".//gml:doubleOrNilReasonTupleList/text()"), document));
|
||||
String[] valuesEntries = valuesText.trim().split("\\s+");
|
||||
logger.trace("valuesText: {}", valuesText);
|
||||
logger.trace("valuesEntries ({}): {}", valuesEntries.length, valuesEntries);
|
||||
if (valuesEntries.length != locations.length * parameters.length * countTimestamps) {
|
||||
throw new FMIUnexpectedResponseException(String.format(
|
||||
"Wrong number of values (%d). Expecting %d * %d * %d = %d", valuesEntries.length, locations.length,
|
||||
parameters.length, countTimestamps, countTimestamps * locations.length * parameters.length));
|
||||
}
|
||||
IntStream.range(0, locations.length).forEach(locationIndex -> {
|
||||
for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) {
|
||||
for (int timestepIndex = 0; timestepIndex < countTimestamps; timestepIndex++) {
|
||||
BigDecimal val = toBigDecimalOrNullIfNaN(
|
||||
valuesEntries[locationIndex * countTimestamps * parameters.length
|
||||
+ timestepIndex * parameters.length + parameterIndex]);
|
||||
logger.trace("Found value {}={} @ time={} for location {}", parameters[parameterIndex], val,
|
||||
timestampsEpoch[timestepIndex], locations[locationIndex].id);
|
||||
builder.appendLocationData(locations[locationIndex], countTimestamps, parameters[parameterIndex],
|
||||
timestampsEpoch[timestepIndex], val);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find representative latitude and longitude matching given xlink href attribute value
|
||||
*
|
||||
* @param xPath xpath object used for query
|
||||
* @param entryIndex index of the location, for logging only on errors
|
||||
* @param document document object
|
||||
* @param href xlink href attribute value. Should start with #
|
||||
* @return latitude and longitude values as array
|
||||
* @throws FMIUnexpectedResponseException parsing errors or when entry is not found
|
||||
* @throws XPathExpressionException xpath errors
|
||||
*/
|
||||
private BigDecimal[] findLatLon(XPath xPath, int entryIndex, Document document, String href)
|
||||
throws FMIUnexpectedResponseException, XPathExpressionException {
|
||||
if (!href.startsWith("#")) {
|
||||
throw new FMIUnexpectedResponseException(
|
||||
"Could not find valid representativePoint xlink:href, does not start with #");
|
||||
}
|
||||
String pointId = href.substring(1);
|
||||
String pointLatLon = takeFirstOrError(String.format("[%d]/pos", entryIndex),
|
||||
queryNodeValues(xPath.compile(".//gml:Point[@gml:id='" + pointId + "']/gml:pos/text()"), document));
|
||||
return parseLatLon(pointLatLon);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse string reprsenting latitude longitude string separated by space
|
||||
*
|
||||
* @param pointLatLon latitude longitude string separated by space
|
||||
* @return latitude and longitude values as array
|
||||
* @throws FMIUnexpectedResponseException on parsing errors
|
||||
*/
|
||||
private BigDecimal[] parseLatLon(String pointLatLon) throws FMIUnexpectedResponseException {
|
||||
String[] latlon = pointLatLon.split(" ");
|
||||
BigDecimal lat, lon;
|
||||
if (latlon.length != 2) {
|
||||
throw new FMIUnexpectedResponseException(String.format(
|
||||
"Invalid latitude or longitude format, expected two values separated by space, got %d values: '%s'",
|
||||
latlon.length, latlon));
|
||||
}
|
||||
try {
|
||||
lat = new BigDecimal(latlon[0]);
|
||||
lon = new BigDecimal(latlon[1]);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new FMIUnexpectedResponseException(
|
||||
String.format("Invalid latitude or longitude format: %s", e.getMessage()));
|
||||
}
|
||||
return new BigDecimal[] { lat, lon };
|
||||
}
|
||||
|
||||
private String[] queryNodeValues(XPathExpression expression, Object source) throws XPathExpressionException {
|
||||
NodeList nodeList = (NodeList) expression.evaluate(source, XPathConstants.NODESET);
|
||||
String[] values = new String[nodeList.getLength()];
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
values[i] = nodeList.item(i).getNodeValue();
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that length of values is exactly 1, and returns it
|
||||
*
|
||||
* @param errorDescription error description for FMIResponseException
|
||||
* @param values
|
||||
* @return
|
||||
* @throws FMIUnexpectedResponseException when length of values != 1
|
||||
*/
|
||||
private String takeFirstOrError(String errorDescription, String[] values) throws FMIUnexpectedResponseException {
|
||||
if (values.length != 1) {
|
||||
throw new FMIUnexpectedResponseException(String.format("No unique match found: %s", errorDescription));
|
||||
}
|
||||
return values[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert string to BigDecimal. "NaN" string is converted to null
|
||||
*
|
||||
* @param value
|
||||
* @return null when value is "NaN". Otherwise BigDecimal representing the string
|
||||
*/
|
||||
private @Nullable BigDecimal toBigDecimalOrNullIfNaN(String value) {
|
||||
if ("NaN".equals(value)) {
|
||||
return null;
|
||||
} else {
|
||||
return new BigDecimal(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate ordering and values of gmlcov:positions (latLonTimeTripletEntries)
|
||||
* essentially
|
||||
* pos1_lat, pos1_lon, time1
|
||||
* pos1_lat, pos1_lon, time2
|
||||
* pos1_lat, pos1_lon, time3
|
||||
* pos2_lat, pos2_lon, time1
|
||||
* pos2_lat, pos2_lon, time2
|
||||
* ..etc..
|
||||
*
|
||||
* - lat, lon should be in correct order and match position entries ("locations")
|
||||
* - time should values should be exactly same for each point (above time1, time2, ...), and match given timestamps
|
||||
* ("timestampsEpoch")
|
||||
*
|
||||
*
|
||||
* @param locations previously discovered locations
|
||||
* @param timestampsEpoch expected timestamps
|
||||
* @param latLonTimeTripletEntries flat array of strings representing the array, [row1_cell1, row1_cell2,
|
||||
* row2_cell1, ...]
|
||||
* @throws FMIUnexpectedResponseException when value ordering is not matching the expected
|
||||
*/
|
||||
private void validatePositionEntries(Location[] locations, long[] timestampsEpoch,
|
||||
String[] latLonTimeTripletEntries) throws FMIUnexpectedResponseException {
|
||||
int countTimestamps = timestampsEpoch.length;
|
||||
for (int locationIndex = 0; locationIndex < locations.length; locationIndex++) {
|
||||
String firstLat = latLonTimeTripletEntries[locationIndex * countTimestamps * 3];
|
||||
String fistLon = latLonTimeTripletEntries[locationIndex * countTimestamps * 3 + 1];
|
||||
|
||||
// step through entries for this position
|
||||
for (int timestepIndex = 0; timestepIndex < countTimestamps; timestepIndex++) {
|
||||
String lat = latLonTimeTripletEntries[locationIndex * countTimestamps * 3 + timestepIndex * 3];
|
||||
String lon = latLonTimeTripletEntries[locationIndex * countTimestamps * 3 + timestepIndex * 3 + 1];
|
||||
String timeEpochSec = latLonTimeTripletEntries[locationIndex * countTimestamps * 3 + timestepIndex * 3
|
||||
+ 2];
|
||||
if (!lat.equals(firstLat) || !lon.equals(fistLon)) {
|
||||
throw new FMIUnexpectedResponseException(String.format(
|
||||
"positions[%d] lat, lon for time index [%d] was not matching expected ordering",
|
||||
locationIndex, timestepIndex));
|
||||
}
|
||||
String expectedLat = locations[locationIndex].latitude.toPlainString();
|
||||
String expectedLon = locations[locationIndex].longitude.toPlainString();
|
||||
if (!lat.equals(expectedLat) || !lon.equals(expectedLon)) {
|
||||
throw new FMIUnexpectedResponseException(String.format(
|
||||
"positions[%d] lat, lon for time index [%d] was not matching representativePoint",
|
||||
locationIndex, timestepIndex));
|
||||
}
|
||||
|
||||
if (Long.parseLong(timeEpochSec) != timestampsEpoch[timestepIndex]) {
|
||||
throw new FMIUnexpectedResponseException(String.format(
|
||||
"positions[%d] time (%s) for time index [%d] was not matching expected (%d) ordering",
|
||||
locationIndex, timeEpochSec, timestepIndex, timestampsEpoch[timestepIndex]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Simple class for numeric holding data
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Data {
|
||||
|
||||
/**
|
||||
* Array of timestamps, as epoch seconds
|
||||
*/
|
||||
public final long[] timestampsEpochSecs;
|
||||
|
||||
/**
|
||||
* Array of values, some of which may be null when value is not present.
|
||||
*/
|
||||
public final @Nullable BigDecimal[] values;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param timestampsEpochSecs
|
||||
* @param values
|
||||
* @throws IllegalArgumentException if length of timestampsEpochSecs and values do not match
|
||||
*/
|
||||
public Data(long[] timestampsEpochSecs, BigDecimal[] values) {
|
||||
if (timestampsEpochSecs.length != values.length) {
|
||||
throw new IllegalArgumentException("length of arguments do not match");
|
||||
}
|
||||
this.timestampsEpochSecs = timestampsEpochSecs;
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("ResponseDataValues(timestampsEpochSecs=").append(Arrays.toString(timestampsEpochSecs))
|
||||
.append(", values=").append(Arrays.deepToString(values)).append(")").toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Class representing response from the FMI weather service
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIResponse {
|
||||
|
||||
private Map<Location, Map<String, Data>> dataByLocationByParameter;
|
||||
|
||||
/**
|
||||
* Builder class for FMIResponse
|
||||
*
|
||||
* @author Sami Salonen
|
||||
* author Sami Salonen - Initial contribution/
|
||||
*
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private Map<Location, Map<String, List<Long>>> timestampsByLocationByParameter;
|
||||
private Map<Location, Map<String, List<@Nullable BigDecimal>>> valuesByLocationByParameter;
|
||||
|
||||
public Builder() {
|
||||
timestampsByLocationByParameter = new HashMap<>();
|
||||
valuesByLocationByParameter = new HashMap<>();
|
||||
}
|
||||
|
||||
public Builder appendLocationData(Location location, @Nullable Integer capacityHintForValues, String parameter,
|
||||
long epochSecond, @Nullable BigDecimal val) {
|
||||
timestampsByLocationByParameter.computeIfAbsent(location, k -> new HashMap<>()).computeIfAbsent(parameter,
|
||||
k -> capacityHintForValues == null ? new ArrayList<>() : new ArrayList<>(capacityHintForValues))
|
||||
.add(epochSecond);
|
||||
valuesByLocationByParameter.computeIfAbsent(location, k -> new HashMap<>()).computeIfAbsent(parameter,
|
||||
k -> capacityHintForValues == null ? new ArrayList<>() : new ArrayList<>(capacityHintForValues))
|
||||
.add(val);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FMIResponse build() {
|
||||
Map<Location, Map<String, Data>> out = new HashMap<>(timestampsByLocationByParameter.size());
|
||||
timestampsByLocationByParameter.entrySet().forEach(entry -> {
|
||||
collectParametersForLocation(out, entry);
|
||||
});
|
||||
return new FMIResponse(out);
|
||||
}
|
||||
|
||||
private void collectParametersForLocation(Map<Location, Map<String, Data>> out,
|
||||
Entry<Location, Map<String, List<Long>>> locationEntry) {
|
||||
Location location = locationEntry.getKey();
|
||||
Map<String, List<Long>> timestampsByParameter = locationEntry.getValue();
|
||||
out.put(location, new HashMap<String, Data>(timestampsByParameter.size()));
|
||||
timestampsByParameter.entrySet().stream().forEach(parameterEntry -> {
|
||||
collectValuesForParameter(out, location, parameterEntry);
|
||||
});
|
||||
}
|
||||
|
||||
private void collectValuesForParameter(Map<Location, Map<String, Data>> out, Location location,
|
||||
Entry<String, List<Long>> parameterEntry) {
|
||||
String parameter = parameterEntry.getKey();
|
||||
long[] timestamps = parameterEntry.getValue().stream().mapToLong(Long::longValue).toArray();
|
||||
BigDecimal[] values = valuesByLocationByParameter.get(location).get(parameter)
|
||||
.toArray(new @Nullable BigDecimal[0]);
|
||||
Data dataValues = new Data(timestamps, values);
|
||||
out.get(location).put(parameter, dataValues);
|
||||
}
|
||||
}
|
||||
|
||||
public FMIResponse(Map<Location, Map<String, Data>> dataByLocationByParameter) {
|
||||
this.dataByLocationByParameter = dataByLocationByParameter;
|
||||
}
|
||||
|
||||
public Optional<Data> getData(Location location, String parameter) {
|
||||
return Optional.ofNullable(dataByLocationByParameter.get(location)).map(paramData -> paramData.get(parameter));
|
||||
}
|
||||
|
||||
public Set<Location> getLocations() {
|
||||
return dataByLocationByParameter.keySet();
|
||||
}
|
||||
|
||||
public Optional<Set<String>> getParameters(Location location) {
|
||||
return Optional.ofNullable(dataByLocationByParameter.get(location)).map(paramData -> paramData.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("FMIResponse(").append(dataByLocationByParameter.toString()).append(")").toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* QueryParameter implementation for fmisid (FMI Station ID) query parameter
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMISID implements QueryParameter {
|
||||
|
||||
private final String fmisid;
|
||||
|
||||
public FMISID(String fmisid) {
|
||||
this.fmisid = fmisid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map.Entry<String, String>> toRequestParameters() {
|
||||
return Collections.singletonList(new AbstractMap.SimpleImmutableEntry<>("fmisid", fmisid));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Request for weather forecasts
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ForecastRequest extends Request {
|
||||
|
||||
public static final String STORED_QUERY_ID = "fmi::forecast::hirlam::surface::point::multipointcoverage";
|
||||
|
||||
// For description of variables: http://opendata.fmi.fi/meta?observableProperty=forecast
|
||||
public static final String PARAM_TEMPERATURE = "Temperature";
|
||||
public static final String PARAM_HUMIDITY = "Humidity";
|
||||
public static final String PARAM_WIND_DIRECTION = "WindDirection";
|
||||
public static final String PARAM_WIND_SPEED = "WindSpeedMS";
|
||||
public static final String PARAM_WIND_GUST = "WindGust";
|
||||
public static final String PARAM_PRESSURE = "Pressure";
|
||||
public static final String PARAM_PRECIPITATION_1H = "Precipitation1h";
|
||||
public static final String PARAM_TOTAL_CLOUD_COVER = "TotalCloudCover";
|
||||
public static final String PARAM_WEATHER_SYMBOL = "WeatherSymbol3";
|
||||
public static final String[] PARAMETERS = new String[] { PARAM_TEMPERATURE, PARAM_HUMIDITY, PARAM_WIND_DIRECTION,
|
||||
PARAM_WIND_SPEED, PARAM_WIND_GUST, PARAM_PRESSURE, PARAM_PRECIPITATION_1H, PARAM_TOTAL_CLOUD_COVER,
|
||||
PARAM_WEATHER_SYMBOL };
|
||||
|
||||
public ForecastRequest(QueryParameter location, long startEpoch, long endEpoch, long timestepMinutes) {
|
||||
super(STORED_QUERY_ID, location, startEpoch, endEpoch, timestepMinutes, PARAMETERS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* QueryParameter implementation for latlon
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class LatLon implements QueryParameter {
|
||||
|
||||
private final String latlon;
|
||||
|
||||
public LatLon(BigDecimal latitude, BigDecimal longitude) {
|
||||
this.latlon = String.format("%s,%s", latitude.toPlainString(), longitude.toPlainString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map.Entry<String, String>> toRequestParameters() {
|
||||
return Collections.singletonList(new AbstractMap.SimpleImmutableEntry<>("latlon", latlon));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Station location
|
||||
*
|
||||
* Note: For simplicity, location implements object equality and hashCode using only id.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Location {
|
||||
|
||||
public final String name;
|
||||
public final String id;
|
||||
public final BigDecimal latitude;
|
||||
public final BigDecimal longitude;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param name name for the location
|
||||
* @param id string identifying this location uniquely. Typically FMISID or latitude-longitude pair
|
||||
* @param latitude latitude of the location
|
||||
* @param longitude longitude of the location
|
||||
*/
|
||||
public Location(String name, String id, BigDecimal latitude, BigDecimal longitude) {
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (!(obj instanceof Location)) {
|
||||
return false;
|
||||
}
|
||||
Location other = (Location) obj;
|
||||
return Objects.equals(id, other.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("Location(name=\"").append(name).append("\", id=\"").append(id).append("\", latitude=")
|
||||
.append(latitude).append(", longitude=").append(longitude).append(")").toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Request for weather observations
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ObservationRequest extends Request {
|
||||
|
||||
public static final String STORED_QUERY_ID = "fmi::observations::weather::multipointcoverage";
|
||||
|
||||
// For description of variables, see http://opendata.fmi.fi/meta?observableProperty=observation
|
||||
public static final String PARAM_TEMPERATURE = "t2m";
|
||||
public static final String PARAM_HUMIDITY = "rh";
|
||||
public static final String PARAM_WIND_DIRECTION = "wd_10min";
|
||||
public static final String PARAM_WIND_SPEED = "ws_10min";
|
||||
public static final String PARAM_WIND_GUST = "wg_10min";
|
||||
public static final String PARAM_PRESSURE = "p_sea";
|
||||
public static final String PARAM_PRECIPITATION_AMOUNT = "r_1h";
|
||||
public static final String PARAM_SNOW_DEPTH = "snow_aws";
|
||||
public static final String PARAM_VISIBILITY = "vis";
|
||||
public static final String PARAM_CLOUDS = "n_man";
|
||||
public static final String PARAM_PRESENT_WEATHER = "wawa";
|
||||
|
||||
public static final String[] PARAMETERS = new String[] { PARAM_TEMPERATURE, PARAM_HUMIDITY, PARAM_WIND_DIRECTION,
|
||||
PARAM_WIND_SPEED, PARAM_WIND_GUST, PARAM_PRESSURE, PARAM_PRECIPITATION_AMOUNT, PARAM_SNOW_DEPTH,
|
||||
PARAM_VISIBILITY, PARAM_CLOUDS, PARAM_PRESENT_WEATHER };
|
||||
|
||||
public ObservationRequest(QueryParameter location, long startEpoch, long endEpoch, long timestepMinutes) {
|
||||
super(STORED_QUERY_ID, location, startEpoch, endEpoch, timestepMinutes, PARAMETERS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Interface for HTTP GET query parameters
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public interface QueryParameter {
|
||||
List<Entry<String, String>> toRequestParameters();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Request class for FIM weather
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class Request {
|
||||
|
||||
public static final String FMI_WFS_URL = "https://opendata.fmi.fi/wfs";
|
||||
|
||||
public final QueryParameter location;
|
||||
public final long startEpoch;
|
||||
public final long endEpoch;
|
||||
public final long timestepMinutes;
|
||||
public final String storedQueryId;
|
||||
public final String[] parameters;
|
||||
|
||||
private static ZoneId UTC = ZoneId.of("Z");
|
||||
private static DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
|
||||
|
||||
public Request(String storedQueryId, QueryParameter location, long startEpoch, long endEpoch,
|
||||
long timestepMinutes) {
|
||||
this(storedQueryId, location, startEpoch, endEpoch, timestepMinutes, new String[0]);
|
||||
}
|
||||
|
||||
public Request(String storedQueryId, QueryParameter location, long startEpoch, long endEpoch, long timestepMinutes,
|
||||
String[] parameters) {
|
||||
this.storedQueryId = storedQueryId;
|
||||
this.location = location;
|
||||
this.startEpoch = startEpoch;
|
||||
this.endEpoch = endEpoch;
|
||||
this.timestepMinutes = timestepMinutes;
|
||||
this.parameters = parameters;
|
||||
}
|
||||
|
||||
public String toUrl() {
|
||||
StringBuilder urlBuilder = new StringBuilder(FMI_WFS_URL)
|
||||
.append("?service=WFS&version=2.0.0&request=getFeature&storedquery_id=").append(storedQueryId)
|
||||
.append("&starttime=").append(epochToIsoDateTime(startEpoch)).append("&endtime=")
|
||||
.append(epochToIsoDateTime(endEpoch)).append("×tep=").append(timestepMinutes);
|
||||
location.toRequestParameters().forEach(entry -> {
|
||||
urlBuilder.append("&").append(entry.getKey()).append("=").append(entry.getValue());
|
||||
});
|
||||
if (parameters.length > 0) {
|
||||
urlBuilder.append("&").append("parameters=").append(String.join(",", parameters));
|
||||
}
|
||||
return urlBuilder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert epoch value (representing UTC time) to ISO formatted date time
|
||||
*
|
||||
* @param epoch
|
||||
* @return
|
||||
*/
|
||||
private static String epochToIsoDateTime(long epoch) {
|
||||
return ZonedDateTime.ofInstant(Instant.ofEpochSecond(epoch), UTC).format(FORMATTER);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client.exception;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Specialized Exception class for ExceptionReport responses from the FMI API
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIExceptionReportException extends FMIResponseException {
|
||||
|
||||
private static final long serialVersionUID = -6402617339310828118L;
|
||||
|
||||
private FMIExceptionReportException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FMIExceptionReportException(String exceptionCode, String[] messages) {
|
||||
this(new StringBuilder("Exception report (").append(exceptionCode).append("): ")
|
||||
.append(Arrays.deepToString(messages)).toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Specialized Exception class for I/O errors related to the FMI API, such as invalid HTTP timeout
|
||||
*
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIIOException extends FMIResponseException {
|
||||
private static final long serialVersionUID = 4835819504565701063L;
|
||||
|
||||
public FMIIOException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FMIIOException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Base class for FMI exceptions
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIResponseException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 938534003881018793L;
|
||||
|
||||
public FMIResponseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FMIResponseException(Throwable e) {
|
||||
super(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.client.exception;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Specialized Exception class for unexpected responses from the FMI API, such as invalid XML or format.
|
||||
*
|
||||
* Different from FMIExceptionReportException which is reserved for explicit error responses from server.
|
||||
*
|
||||
* @see FMIExceptionReportException
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIUnexpectedResponseException extends FMIResponseException {
|
||||
|
||||
private static final long serialVersionUID = 5068780757336770041L;
|
||||
|
||||
public FMIUnexpectedResponseException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FMIUnexpectedResponseException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.discovery;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
|
||||
/**
|
||||
* Cities of Finland
|
||||
*
|
||||
* Parsed from
|
||||
* https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::hirlam::surface::cities::multipointcoverage
|
||||
*
|
||||
* Using piece of code similar to below:
|
||||
*
|
||||
* <pre>
|
||||
* System.out.println(parseMultiPointCoverageXml(new String(
|
||||
* Files.readAllBytes(getTestResource("forecast_hirlam_surface_cities_multipointcoverage_response.xml"))))
|
||||
* .getLocations());
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public final class CitiesOfFinland {
|
||||
|
||||
public static final Set<Location> CITIES_OF_FINLAND = new HashSet<>();
|
||||
static {
|
||||
CITIES_OF_FINLAND
|
||||
.add(new Location("Akaa", "61.16667,23.86667", new BigDecimal("61.16667"), new BigDecimal("23.86667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Alajärvi", "63.00000,23.81667", new BigDecimal("63.00000"), new BigDecimal("23.81667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Alavus", "62.58333,23.61667", new BigDecimal("62.58333"), new BigDecimal("23.61667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Espoo", "60.20520,24.65220", new BigDecimal("60.20520"), new BigDecimal("24.65220")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Forssa", "60.81462,23.62146", new BigDecimal("60.81462"), new BigDecimal("23.62146")));
|
||||
CITIES_OF_FINLAND.add(new Location("Haapajärvi", "63.75000,25.33333", new BigDecimal("63.75000"),
|
||||
new BigDecimal("25.33333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Haapavesi", "64.12507,25.34792", new BigDecimal("64.12507"), new BigDecimal("25.34792")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Hamina", "60.56974,27.19794", new BigDecimal("60.56974"), new BigDecimal("27.19794")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Hanko", "59.83333,22.95000", new BigDecimal("59.83333"), new BigDecimal("22.95000")));
|
||||
CITIES_OF_FINLAND.add(new Location("Harjavalta", "61.31667,22.13333", new BigDecimal("61.31667"),
|
||||
new BigDecimal("22.13333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Heinola", "61.20564,26.03811", new BigDecimal("61.20564"), new BigDecimal("26.03811")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Helsinki", "60.16952,24.93545", new BigDecimal("60.16952"), new BigDecimal("24.93545")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Huittinen", "61.18333,22.70000", new BigDecimal("61.18333"), new BigDecimal("22.70000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Hyvinkää", "60.63333,24.86667", new BigDecimal("60.63333"), new BigDecimal("24.86667")));
|
||||
CITIES_OF_FINLAND.add(new Location("Hämeenlinna", "60.99596,24.46434", new BigDecimal("60.99596"),
|
||||
new BigDecimal("24.46434")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Iisalmi", "63.55915,27.19067", new BigDecimal("63.55915"), new BigDecimal("27.19067")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Ikaalinen", "61.76951,23.06580", new BigDecimal("61.76951"), new BigDecimal("23.06580")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Imatra", "61.17185,28.75242", new BigDecimal("61.17185"), new BigDecimal("28.75242")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Jakobstad", "63.67486,22.70256", new BigDecimal("63.67486"), new BigDecimal("22.70256")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Joensuu", "62.60118,29.76316", new BigDecimal("62.60118"), new BigDecimal("29.76316")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Juankoski", "63.06667,28.35000", new BigDecimal("63.06667"), new BigDecimal("28.35000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Jyvaskyla", "62.24147,25.72088", new BigDecimal("62.24147"), new BigDecimal("25.72088")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Jämsä", "61.86420,25.19002", new BigDecimal("61.86420"), new BigDecimal("25.19002")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Järvenpää", "60.47369,25.08992", new BigDecimal("60.47369"), new BigDecimal("25.08992")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kaarina", "60.40724,22.36904", new BigDecimal("60.40724"), new BigDecimal("22.36904")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kajaani", "64.22728,27.72846", new BigDecimal("64.22728"), new BigDecimal("27.72846")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kalajoki", "64.25000,23.95000", new BigDecimal("64.25000"), new BigDecimal("23.95000")));
|
||||
CITIES_OF_FINLAND.add(new Location("Kankaanpää", "61.80000,22.41667", new BigDecimal("61.80000"),
|
||||
new BigDecimal("22.41667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kannus", "63.90000,23.90000", new BigDecimal("63.90000"), new BigDecimal("23.90000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Karkkila", "60.53418,24.20977", new BigDecimal("60.53418"), new BigDecimal("24.20977")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kaskinen", "62.38330,21.21670", new BigDecimal("62.38330"), new BigDecimal("21.21670")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kauhajoki", "62.43333,22.18333", new BigDecimal("62.43333"), new BigDecimal("22.18333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kauhava", "63.10299,23.07129", new BigDecimal("63.10299"), new BigDecimal("23.07129")));
|
||||
CITIES_OF_FINLAND.add(new Location("Kauniainen", "60.21209,24.72756", new BigDecimal("60.21209"),
|
||||
new BigDecimal("24.72756")));
|
||||
CITIES_OF_FINLAND
|
||||
.add(new Location("Kemi", "65.75000,24.58333", new BigDecimal("65.75000"), new BigDecimal("24.58333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kemijärvi", "66.66667,27.41667", new BigDecimal("66.66667"), new BigDecimal("27.41667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kerava", "60.40338,25.10500", new BigDecimal("60.40338"), new BigDecimal("25.10500")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Keuruu", "62.26667,24.70000", new BigDecimal("62.26667"), new BigDecimal("24.70000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kitee", "62.10000,30.15000", new BigDecimal("62.10000"), new BigDecimal("30.15000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kiuruvesi", "63.65000,26.61667", new BigDecimal("63.65000"), new BigDecimal("26.61667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kokemäki", "61.25647,22.35643", new BigDecimal("61.25647"), new BigDecimal("22.35643")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kokkola", "63.83847,23.13066", new BigDecimal("63.83847"), new BigDecimal("23.13066")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kotka", "60.46667,26.91667", new BigDecimal("60.46667"), new BigDecimal("26.91667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kouvola", "60.86667,26.70000", new BigDecimal("60.86667"), new BigDecimal("26.70000")));
|
||||
CITIES_OF_FINLAND.add(new Location("Kristinestad", "62.27429,21.37596", new BigDecimal("62.27429"),
|
||||
new BigDecimal("21.37596")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kuhmo", "64.13333,29.51667", new BigDecimal("64.13333"), new BigDecimal("29.51667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kuopio", "62.89238,27.67703", new BigDecimal("62.89238"), new BigDecimal("27.67703")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kurikka", "62.61667,22.41667", new BigDecimal("62.61667"), new BigDecimal("22.41667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Kuusamo", "65.96667,29.18333", new BigDecimal("65.96667"), new BigDecimal("29.18333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Lahti", "60.98267,25.66151", new BigDecimal("60.98267"), new BigDecimal("25.66151")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Laitila", "60.87575,21.69765", new BigDecimal("60.87575"), new BigDecimal("21.69765")));
|
||||
CITIES_OF_FINLAND.add(new Location("Lappeenranta", "61.05871,28.18871", new BigDecimal("61.05871"),
|
||||
new BigDecimal("28.18871")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Lapua", "62.96927,23.00880", new BigDecimal("62.96927"), new BigDecimal("23.00880")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Lieksa", "63.31667,30.01667", new BigDecimal("63.31667"), new BigDecimal("30.01667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Lohja", "60.24859,24.06534", new BigDecimal("60.24859"), new BigDecimal("24.06534")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Loimaa", "60.84972,23.05610", new BigDecimal("60.84972"), new BigDecimal("23.05610")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Loviisa", "60.45659,26.22505", new BigDecimal("60.45659"), new BigDecimal("26.22505")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Mariehamn", "60.09726,19.93481", new BigDecimal("60.09726"), new BigDecimal("19.93481")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Mikkeli", "61.68857,27.27227", new BigDecimal("61.68857"), new BigDecimal("27.27227")));
|
||||
CITIES_OF_FINLAND.add(new Location("Mänttä-Vilppula", "62.02966,24.60268", new BigDecimal("62.02966"),
|
||||
new BigDecimal("24.60268")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Naantali", "60.46744,22.02428", new BigDecimal("60.46744"), new BigDecimal("22.02428")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Nilsiä", "63.20000,28.08333", new BigDecimal("63.20000"), new BigDecimal("28.08333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Nivala", "63.91667,24.96667", new BigDecimal("63.91667"), new BigDecimal("24.96667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Nokia", "61.46667,23.50000", new BigDecimal("61.46667"), new BigDecimal("23.50000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Nurmes", "63.54205,29.13965", new BigDecimal("63.54205"), new BigDecimal("29.13965")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Nykarleby", "63.52277,22.53073", new BigDecimal("63.52277"), new BigDecimal("22.53073")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Närpes", "62.47283,21.33707", new BigDecimal("62.47283"), new BigDecimal("21.33707")));
|
||||
CITIES_OF_FINLAND.add(new Location("Orimattila", "60.80487,25.72964", new BigDecimal("60.80487"),
|
||||
new BigDecimal("25.72964")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Orivesi", "61.67766,24.35720", new BigDecimal("61.67766"), new BigDecimal("24.35720")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Oulainen", "64.26667,24.80000", new BigDecimal("64.26667"), new BigDecimal("24.80000")));
|
||||
CITIES_OF_FINLAND
|
||||
.add(new Location("Oulu", "65.01236,25.46816", new BigDecimal("65.01236"), new BigDecimal("25.46816")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Outokumpu", "62.72685,29.01592", new BigDecimal("62.72685"), new BigDecimal("29.01592")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Paimio", "60.45671,22.68694", new BigDecimal("60.45671"), new BigDecimal("22.68694")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Pargas", "60.00000,23.15000", new BigDecimal("60.00000"), new BigDecimal("23.15000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Parkano", "62.01667,23.01667", new BigDecimal("62.01667"), new BigDecimal("23.01667")));
|
||||
CITIES_OF_FINLAND.add(new Location("Pieksämäki", "62.30000,27.13333", new BigDecimal("62.30000"),
|
||||
new BigDecimal("27.13333")));
|
||||
CITIES_OF_FINLAND
|
||||
.add(new Location("Pori", "61.48333,21.78333", new BigDecimal("61.48333"), new BigDecimal("21.78333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Porvoo", "60.39233,25.66507", new BigDecimal("60.39233"), new BigDecimal("25.66507")));
|
||||
CITIES_OF_FINLAND.add(new Location("Pudasjärvi", "65.38333,26.91667", new BigDecimal("65.38333"),
|
||||
new BigDecimal("26.91667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Pyhäjärvi", "63.66667,25.90000", new BigDecimal("63.66667"), new BigDecimal("25.90000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Raahe", "64.68333,24.48333", new BigDecimal("64.68333"), new BigDecimal("24.48333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Raisio", "60.48592,22.16895", new BigDecimal("60.48592"), new BigDecimal("22.16895")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Raseborg", "59.97735,23.43967", new BigDecimal("59.97735"), new BigDecimal("23.43967")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Rauma", "61.12724,21.51127", new BigDecimal("61.12724"), new BigDecimal("21.51127")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Riihimäki", "60.73769,24.77726", new BigDecimal("60.73769"), new BigDecimal("24.77726")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Rovaniemi", "66.50000,25.71667", new BigDecimal("66.50000"), new BigDecimal("25.71667")));
|
||||
CITIES_OF_FINLAND.add(new Location("Saarijärvi", "62.70486,25.25396", new BigDecimal("62.70486"),
|
||||
new BigDecimal("25.25396")));
|
||||
CITIES_OF_FINLAND
|
||||
.add(new Location("Salo", "60.38333,23.13333", new BigDecimal("60.38333"), new BigDecimal("23.13333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Sastamala", "61.35021,22.91053", new BigDecimal("61.35021"), new BigDecimal("22.91053")));
|
||||
CITIES_OF_FINLAND.add(new Location("Savonlinna", "61.86990,28.87999", new BigDecimal("61.86990"),
|
||||
new BigDecimal("28.87999")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Seinäjoki", "62.79446,22.82822", new BigDecimal("62.79446"), new BigDecimal("22.82822")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Somero", "60.61667,23.53333", new BigDecimal("60.61667"), new BigDecimal("23.53333")));
|
||||
CITIES_OF_FINLAND.add(new Location("Suonenjoki", "62.61667,27.13333", new BigDecimal("62.61667"),
|
||||
new BigDecimal("27.13333")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Tampere", "61.49911,23.78712", new BigDecimal("61.49911"), new BigDecimal("23.78712")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Tornio", "65.84811,24.14662", new BigDecimal("65.84811"), new BigDecimal("24.14662")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Turku", "60.45148,22.26869", new BigDecimal("60.45148"), new BigDecimal("22.26869")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Ulvila", "61.42844,21.87103", new BigDecimal("61.42844"), new BigDecimal("21.87103")));
|
||||
CITIES_OF_FINLAND.add(new Location("Uusikaupunki", "60.80043,21.40841", new BigDecimal("60.80043"),
|
||||
new BigDecimal("21.40841")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Vaasa", "63.09600,21.61577", new BigDecimal("63.09600"), new BigDecimal("21.61577")));
|
||||
CITIES_OF_FINLAND.add(new Location("Valkeakoski", "61.26421,24.03122", new BigDecimal("61.26421"),
|
||||
new BigDecimal("24.03122")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Vantaa", "60.30000,24.85000", new BigDecimal("60.30000"), new BigDecimal("24.85000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Varkaus", "62.31533,27.87300", new BigDecimal("62.31533"), new BigDecimal("27.87300")));
|
||||
CITIES_OF_FINLAND.add(new Location("Viitasaari", "63.06667,25.86667", new BigDecimal("63.06667"),
|
||||
new BigDecimal("25.86667")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Vilppula", "62.02121,24.50483", new BigDecimal("62.02121"), new BigDecimal("24.50483")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Virrat", "62.24759,23.78004", new BigDecimal("62.24759"), new BigDecimal("23.78004")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Ylivieska", "64.08333,24.55000", new BigDecimal("64.08333"), new BigDecimal("24.55000")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Ylöjärvi", "61.55632,23.59606", new BigDecimal("61.55632"), new BigDecimal("23.59606")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Ähtäri", "62.55403,24.06186", new BigDecimal("62.55403"), new BigDecimal("24.06186")));
|
||||
CITIES_OF_FINLAND.add(
|
||||
new Location("Äänekoski", "62.60000,25.73333", new BigDecimal("62.60000"), new BigDecimal("25.73333")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.fmiweather.internal.BindingConstants.*;
|
||||
import static org.openhab.binding.fmiweather.internal.discovery.CitiesOfFinland.CITIES_OF_FINLAND;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.fmiweather.internal.BindingConstants;
|
||||
import org.openhab.binding.fmiweather.internal.client.Client;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIResponseException;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIUnexpectedResponseException;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
import org.openhab.core.config.discovery.DiscoveryResult;
|
||||
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
|
||||
import org.openhab.core.config.discovery.DiscoveryService;
|
||||
import org.openhab.core.i18n.LocationProvider;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
import org.openhab.core.thing.ThingUID;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.Reference;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link FMIDiscoveryService} creates things based on the configured location.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.fmiweather")
|
||||
public class FMIWeatherDiscoveryService extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(FMIWeatherDiscoveryService.class);
|
||||
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_OBSERVATION);
|
||||
private static final long STATIONS_CACHE_MILLIS = TimeUnit.HOURS.toMillis(12);
|
||||
private static final int STATIONS_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(10);
|
||||
private static final int DISCOVER_TIMEOUT_SECONDS = 5;
|
||||
private static final int LOCATION_CHANGED_CHECK_INTERVAL_SECONDS = 60;
|
||||
private static final int FIND_STATION_METERS = 80_000;
|
||||
|
||||
private @Nullable ScheduledFuture<?> discoveryJob;
|
||||
private @Nullable PointType previousLocation;
|
||||
private @NonNullByDefault({}) LocationProvider locationProvider;
|
||||
|
||||
private ExpiringCache<Set<Location>> stationsCache = new ExpiringCache<>(STATIONS_CACHE_MILLIS, () -> {
|
||||
try {
|
||||
return new Client().queryWeatherStations(STATIONS_TIMEOUT_MILLIS);
|
||||
} catch (FMIUnexpectedResponseException e) {
|
||||
logger.warn("Unexpected error with the response, potentially API format has changed. Printing out details",
|
||||
e);
|
||||
} catch (FMIResponseException e) {
|
||||
logger.warn("Error when querying stations, {}: {}", e.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
// Return empty set on errors
|
||||
return Collections.emptySet();
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a {@link FMIWeatherDiscoveryService} with immediately enabled background discovery.
|
||||
*/
|
||||
public FMIWeatherDiscoveryService() {
|
||||
super(SUPPORTED_THING_TYPES, DISCOVER_TIMEOUT_SECONDS, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
PointType location = null;
|
||||
logger.debug("Starting FMI Weather discovery scan");
|
||||
LocationProvider locationProvider = getLocationProvider();
|
||||
location = locationProvider.getLocation();
|
||||
if (location == null) {
|
||||
logger.debug("LocationProvider.getLocation() is not set -> Will discover all stations");
|
||||
}
|
||||
createResults(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
if (discoveryJob == null) {
|
||||
discoveryJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
PointType currentLocation = locationProvider.getLocation();
|
||||
if (!Objects.equals(currentLocation, previousLocation)) {
|
||||
logger.debug("Location has been changed from {} to {}: Creating new discovery results",
|
||||
previousLocation, currentLocation);
|
||||
createResults(currentLocation);
|
||||
previousLocation = currentLocation;
|
||||
}
|
||||
}, 0, LOCATION_CHANGED_CHECK_INTERVAL_SECONDS, TimeUnit.SECONDS);
|
||||
logger.debug("Scheduled FMI Weather location-changed discovery job every {} seconds",
|
||||
LOCATION_CHANGED_CHECK_INTERVAL_SECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
public void createResults(@Nullable PointType location) {
|
||||
createForecastForCurrentLocation(location);
|
||||
createForecastsForCities(location);
|
||||
createObservationsForStations(location);
|
||||
}
|
||||
|
||||
private void createForecastForCurrentLocation(@Nullable PointType currentLocation) {
|
||||
if (currentLocation != null) {
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(UID_LOCAL_FORECAST)
|
||||
.withLabel(String.format("FMI local weather forecast"))
|
||||
.withProperty(LOCATION,
|
||||
String.format("%s,%s", currentLocation.getLatitude(), currentLocation.getLongitude()))
|
||||
.withRepresentationProperty(LOCATION).build();
|
||||
thingDiscovered(discoveryResult);
|
||||
}
|
||||
}
|
||||
|
||||
private void createForecastsForCities(@Nullable PointType currentLocation) {
|
||||
CITIES_OF_FINLAND.stream().filter(location2 -> isClose(currentLocation, location2)).forEach(city -> {
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder
|
||||
.create(new ThingUID(THING_TYPE_FORECAST, cleanId(String.format("city_%s", city.name))))
|
||||
.withProperty(LOCATION,
|
||||
String.format("%s,%s", city.latitude.toPlainString(), city.longitude.toPlainString()))
|
||||
.withLabel(String.format("FMI weather forecast for %s", city.name))
|
||||
.withRepresentationProperty(LOCATION).build();
|
||||
thingDiscovered(discoveryResult);
|
||||
});
|
||||
}
|
||||
|
||||
private void createObservationsForStations(@Nullable PointType location) {
|
||||
List<Location> candidateStations = new LinkedList<>();
|
||||
List<Location> filteredStations = new LinkedList<>();
|
||||
cachedStations().peek(station -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
candidateStations.add(station);
|
||||
}
|
||||
}).filter(location2 -> isClose(location, location2)).peek(station -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
filteredStations.add(station);
|
||||
}
|
||||
}).forEach(station -> {
|
||||
DiscoveryResult discoveryResult = DiscoveryResultBuilder
|
||||
.create(new ThingUID(THING_TYPE_OBSERVATION,
|
||||
cleanId(String.format("station_%s_%s", station.id, station.name))))
|
||||
.withLabel(String.format("FMI weather observation for %s", station.name))
|
||||
.withProperty(BindingConstants.FMISID, station.id)
|
||||
.withRepresentationProperty(BindingConstants.FMISID).build();
|
||||
thingDiscovered(discoveryResult);
|
||||
});
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Candidate stations: {}",
|
||||
candidateStations.stream().map(station -> String.format("%s (%s)", station.name, station.id))
|
||||
.collect(Collectors.toCollection(TreeSet<String>::new)));
|
||||
logger.debug("Filtered stations: {}",
|
||||
filteredStations.stream().map(station -> String.format("%s (%s)", station.name, station.id))
|
||||
.collect(Collectors.toCollection(TreeSet<String>::new)));
|
||||
}
|
||||
}
|
||||
|
||||
private static String cleanId(String id) {
|
||||
return id.replace("ä", "a").replace("ö", "o").replace("å", "a").replace("Ä", "A").replace("Ö", "O")
|
||||
.replace("Å", "a").replaceAll("[^a-zA-Z0-9_]", "_");
|
||||
}
|
||||
|
||||
private static boolean isClose(@Nullable PointType location, Location location2) {
|
||||
return location == null ? true
|
||||
: new PointType(new DecimalType(location2.latitude), new DecimalType(location2.longitude))
|
||||
.distanceFrom(location).doubleValue() < FIND_STATION_METERS;
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
private Stream<Location> cachedStations() {
|
||||
Set<Location> stations = stationsCache.getValue();
|
||||
if (stations.isEmpty()) {
|
||||
stationsCache.invalidateValue();
|
||||
}
|
||||
return stationsCache.getValue().stream();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
logger.debug("Stopping FMI Weather background discovery");
|
||||
ScheduledFuture<?> discoveryJob = this.discoveryJob;
|
||||
if (discoveryJob != null) {
|
||||
if (discoveryJob.cancel(true)) {
|
||||
this.discoveryJob = null;
|
||||
logger.debug("Stopped FMI Weather background discovery");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setLocationProvider(LocationProvider locationProvider) {
|
||||
this.locationProvider = locationProvider;
|
||||
}
|
||||
|
||||
protected void unsetLocationProvider(LocationProvider provider) {
|
||||
this.locationProvider = null;
|
||||
}
|
||||
|
||||
protected LocationProvider getLocationProvider() {
|
||||
return locationProvider;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="fmiweather" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
|
||||
|
||||
<name>FMI Weather Binding</name>
|
||||
<description>This is the binding for Finnish Meteorological Institute (FMI) Weather</description>
|
||||
<author>Sami Salonen</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,531 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="fmiweather"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
|
||||
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
|
||||
|
||||
<!-- Things -->
|
||||
<thing-type id="observation">
|
||||
<label>FMI Current Weather (Observation)</label>
|
||||
<description>Finnish Meteorological Institute (FMI) weather observation</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="current" typeId="current-group"/>
|
||||
</channel-groups>
|
||||
|
||||
<config-description>
|
||||
<parameter name="fmisid" type="integer" min="100000" required="true">
|
||||
<label>FMISID of the Weather Station</label>
|
||||
<description><![CDATA[Station ID (FMISID) of the weather observation station
|
||||
<br />
|
||||
<br />See https://en.ilmatieteenlaitos.fi/observation-stations for a list of observation stations. Select 'Weather' station for widest set of observations.]]></description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
<thing-type id="forecast">
|
||||
<label>FMI Weather Forecast</label>
|
||||
<description>Finnish Meteorological Institute (FMI) weather forecast</description>
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="forecastNow" typeId="group-forecast">
|
||||
<label>Forecast for the Current Time</label>
|
||||
<description>This is the weather forecast for the current time</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours01" typeId="group-forecast">
|
||||
<label>1 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 1 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours02" typeId="group-forecast">
|
||||
<label>2 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 2 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours03" typeId="group-forecast">
|
||||
<label>3 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 3 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours04" typeId="group-forecast">
|
||||
<label>4 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 4 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours05" typeId="group-forecast">
|
||||
<label>5 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 5 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours06" typeId="group-forecast">
|
||||
<label>6 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 6 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours07" typeId="group-forecast">
|
||||
<label>7 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 7 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours08" typeId="group-forecast">
|
||||
<label>8 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 8 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours09" typeId="group-forecast">
|
||||
<label>9 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 9 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours10" typeId="group-forecast">
|
||||
<label>10 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 10 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours11" typeId="group-forecast">
|
||||
<label>11 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 11 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours12" typeId="group-forecast">
|
||||
<label>12 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 12 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours13" typeId="group-forecast">
|
||||
<label>13 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 13 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours14" typeId="group-forecast">
|
||||
<label>14 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 14 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours15" typeId="group-forecast">
|
||||
<label>15 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 15 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours16" typeId="group-forecast">
|
||||
<label>16 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 16 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours17" typeId="group-forecast">
|
||||
<label>17 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 17 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours18" typeId="group-forecast">
|
||||
<label>18 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 18 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours19" typeId="group-forecast">
|
||||
<label>19 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 19 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours20" typeId="group-forecast">
|
||||
<label>20 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 20 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours21" typeId="group-forecast">
|
||||
<label>21 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 21 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours22" typeId="group-forecast">
|
||||
<label>22 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 22 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours23" typeId="group-forecast">
|
||||
<label>23 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 23 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours24" typeId="group-forecast">
|
||||
<label>24 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 24 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours25" typeId="group-forecast">
|
||||
<label>25 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 25 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours26" typeId="group-forecast">
|
||||
<label>26 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 26 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours27" typeId="group-forecast">
|
||||
<label>27 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 27 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours28" typeId="group-forecast">
|
||||
<label>28 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 28 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours29" typeId="group-forecast">
|
||||
<label>29 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 29 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours30" typeId="group-forecast">
|
||||
<label>30 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 30 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours31" typeId="group-forecast">
|
||||
<label>31 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 31 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours32" typeId="group-forecast">
|
||||
<label>32 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 32 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours33" typeId="group-forecast">
|
||||
<label>33 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 33 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours34" typeId="group-forecast">
|
||||
<label>34 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 34 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours35" typeId="group-forecast">
|
||||
<label>35 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 35 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours36" typeId="group-forecast">
|
||||
<label>36 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 36 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours37" typeId="group-forecast">
|
||||
<label>37 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 37 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours38" typeId="group-forecast">
|
||||
<label>38 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 38 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours39" typeId="group-forecast">
|
||||
<label>39 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 39 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours40" typeId="group-forecast">
|
||||
<label>40 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 40 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours41" typeId="group-forecast">
|
||||
<label>41 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 41 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours42" typeId="group-forecast">
|
||||
<label>42 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 42 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours43" typeId="group-forecast">
|
||||
<label>43 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 43 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours44" typeId="group-forecast">
|
||||
<label>44 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 44 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours45" typeId="group-forecast-advanced">
|
||||
<label>45 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 45 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours46" typeId="group-forecast-advanced">
|
||||
<label>46 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 46 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours47" typeId="group-forecast-advanced">
|
||||
<label>47 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 47 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours48" typeId="group-forecast-advanced">
|
||||
<label>48 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 48 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours49" typeId="group-forecast-advanced">
|
||||
<label>49 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 49 hours.</description>
|
||||
</channel-group>
|
||||
<channel-group id="forecastHours50" typeId="group-forecast-advanced">
|
||||
<label>50 Hours Forecast</label>
|
||||
<description>This is the weather forecast in 50 hours.</description>
|
||||
</channel-group>
|
||||
</channel-groups>
|
||||
|
||||
<config-description>
|
||||
<parameter name="location" type="text" required="true">
|
||||
<label>Location</label>
|
||||
<description>Location of weather in geographical coordinates (latitude,longitude).</description>
|
||||
</parameter>
|
||||
</config-description>
|
||||
|
||||
</thing-type>
|
||||
|
||||
|
||||
<!-- Groups -->
|
||||
<channel-group-type id="current-group">
|
||||
<label>Current Weather</label>
|
||||
<description>This is the current weather.</description>
|
||||
<channels>
|
||||
<channel id="time" typeId="time-channel"/>
|
||||
<channel id="temperature" typeId="temperature-channel"/>
|
||||
<channel id="humidity" typeId="humidity-channel"/>
|
||||
<channel id="wind-direction" typeId="wind-direction-channel"/>
|
||||
<channel id="wind-speed" typeId="wind-speed-channel"/>
|
||||
<channel id="wind-gust" typeId="wind-gust-channel"/>
|
||||
<channel id="pressure" typeId="pressure-channel"/>
|
||||
<channel id="precipitation" typeId="precipitation-channel"/>
|
||||
<channel id="snow-depth" typeId="snow-depth-channel"/>
|
||||
<channel id="visibility" typeId="visibility-channel"/>
|
||||
<channel id="clouds" typeId="clouds-channel"/>
|
||||
<channel id="present-weather" typeId="present-weather-channel"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
<channel-group-type id="group-forecast">
|
||||
<label>Forecast</label>
|
||||
<description>This is hourly weather forecast.</description>
|
||||
<channels>
|
||||
<channel id="time" typeId="forecast-time-channel"/>
|
||||
<channel id="temperature" typeId="temperature-channel"/>
|
||||
<channel id="humidity" typeId="humidity-channel"/>
|
||||
<channel id="wind-direction" typeId="wind-direction-channel"/>
|
||||
<channel id="wind-speed" typeId="wind-speed-channel"/>
|
||||
<channel id="wind-gust" typeId="wind-gust-channel"/>
|
||||
<channel id="pressure" typeId="pressure-channel"/>
|
||||
<channel id="precipitation-intensity" typeId="precipitation-intensity-channel"/>
|
||||
<channel id="total-cloud-cover" typeId="total-cloud-cover-channel"/>
|
||||
<channel id="weather-id" typeId="weather-id-channel"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
<channel-group-type id="group-forecast-advanced" advanced="true">
|
||||
<label>Forecast</label>
|
||||
<description>This is hourly weather forecast.</description>
|
||||
<channels>
|
||||
<channel id="time" typeId="forecast-time-channel"/>
|
||||
<channel id="temperature" typeId="temperature-channel"/>
|
||||
<channel id="humidity" typeId="humidity-channel"/>
|
||||
<channel id="wind-direction" typeId="wind-direction-channel"/>
|
||||
<channel id="wind-speed" typeId="wind-speed-channel"/>
|
||||
<channel id="wind-gust" typeId="wind-gust-channel"/>
|
||||
<channel id="pressure" typeId="pressure-channel"/>
|
||||
<channel id="precipitation-intensity" typeId="precipitation-intensity-channel"/>
|
||||
<channel id="total-cloud-cover" typeId="total-cloud-cover-channel"/>
|
||||
<channel id="weather-id" typeId="weather-id-channel"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
|
||||
<!-- Channel types -->
|
||||
<!-- Some descriptions from https://en.ilmatieteenlaitos.fi/guidance-to-observations -->
|
||||
<channel-type id="time-channel">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Observation Time</label>
|
||||
<category>Time</category>
|
||||
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
|
||||
</channel-type>
|
||||
<channel-type id="forecast-time-channel">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Forecast Time</label>
|
||||
<category>Time</category>
|
||||
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
|
||||
</channel-type>
|
||||
<channel-type id="temperature-channel">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<category>Temperature</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="humidity-channel">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Humidity</label>
|
||||
<category>Humidity</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="wind-direction-channel">
|
||||
<item-type>Number:Angle</item-type>
|
||||
<label>Wind Direction</label>
|
||||
<category>Wind</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="wind-speed-channel">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Wind Speed</label>
|
||||
<category>Wind</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="wind-gust-channel">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Wind Gust</label>
|
||||
<category>Wind</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="pressure-channel">
|
||||
<item-type>Number:Pressure</item-type>
|
||||
<label>Pressure</label>
|
||||
<category>Pressure</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="precipitation-channel">
|
||||
<item-type>Number:Length</item-type>
|
||||
<label>Precipitation</label>
|
||||
<description>Precipitation in one hour</description>
|
||||
<category>Rain</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="snow-depth-channel">
|
||||
<item-type>Number:Length</item-type>
|
||||
<label>Snow depth</label>
|
||||
<category>Snow</category>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="visibility-channel">
|
||||
<item-type>Number:Length</item-type>
|
||||
<label>Visibility</label>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="clouds-channel">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Cloudiness</label>
|
||||
<description>Given as percentage, 0% being clear skies, and 100% being overcast. UNDEF when cloud coverage could not
|
||||
be determined.
|
||||
Takes into account all cloud layers.</description>
|
||||
<state readOnly="true" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="present-weather-channel">
|
||||
<item-type>Number</item-type>
|
||||
<label>Prevailing Weather</label>
|
||||
<description>Prevailing weather code (WaWa field) according to WMO code 4680. For Finnish explanation, consult
|
||||
https://www.ilmatieteenlaitos.fi/latauspalvelun-pikaohje</description>
|
||||
<state readOnly="true" min="0" max="99" pattern="%s">
|
||||
<options>
|
||||
<!-- This corresponds to WaWa field in observations -->
|
||||
<!-- WaWa refers to WMO code 4680 (google it) -->
|
||||
<!-- See e.g. https://helda.helsinki.fi/bitstream/handle/10138/37284/PRO_GRADU_BOOK_HERMAN.pdf?sequence=2 -->
|
||||
<option value="00">No significant weather observed</option>
|
||||
<option value="01">Clouds generally dissolving or becoming less developed during the past hour</option>
|
||||
<option value="02">State of sky on the whole unchanged during the past hour</option>
|
||||
<option value="03">Clouds generally forming or developing during the past hour</option>
|
||||
<option value="04">Haze or smoke, or dust in suspension in the air, visibility equal to, or greater than, 1 km</option>
|
||||
<option value="05">Haze or smoke, or dust in suspension in the air, visibility less than 1 km</option>
|
||||
<!-- 6-9: Reserved -->
|
||||
<option value="10">Mist</option>
|
||||
<option value="11">Diamond dust</option>
|
||||
<option value="12">Distant lightning</option>
|
||||
<!-- 13-17: Reserved -->
|
||||
<option value="18">Squalls</option>
|
||||
<option value="19">Reserved</option>
|
||||
<!-- Code figures 20–26 are used to report precipitation, fog (or ice fog) or thunderstorm at the station during the
|
||||
preceding hour but not at the time of observation -->
|
||||
<option value="20">Fog</option>
|
||||
<option value="21">Precipitation</option>
|
||||
<option value="22">Drizzle (not freezing) or snow grains</option>
|
||||
<option value="23">Rain (not freezing)</option>
|
||||
<option value="24">Snow</option>
|
||||
<option value="25">Freezing drizzle or freezing rain</option>
|
||||
<option value="26">Thunderstorm (with or without precipitation)</option>
|
||||
<option value="27">Blowing or drifting snow or sand</option>
|
||||
<option value="28">Blowing or drifting snow or sand, visibility equal to, or greater than, 1 km</option>
|
||||
<option value="29">Blowing or drifting snow or sand, visibility less than 1 km</option>
|
||||
<option value="30">Fog</option>
|
||||
<option value="31">Fog or ice fog in patches</option>
|
||||
<option value="32">Fog or ice fog, has become thinner during the past hour</option>
|
||||
<option value="33">Fog or ice fog, no appreciable change during the past hour</option>
|
||||
<option value="34">Fog or ice fog, has begun or become thicker during the past hour</option>
|
||||
<option value="35">Fog, depositing rime</option>
|
||||
<!-- 36-39: Reserved -->
|
||||
<option value="40">Precipitation</option>
|
||||
<option value="41">Precipitation, slight or moderate</option>
|
||||
<option value="42">Precipitation, heavy</option>
|
||||
<option value="43">Liquid precipitation, slight or moderate</option>
|
||||
<option value="44">Liquid precipitation, heavy</option>
|
||||
<option value="45">Solid precipitation, slight or moderate</option>
|
||||
<option value="46">Solid precipitation, heavy</option>
|
||||
<option value="47">Freezing precipitation, slight or moderate</option>
|
||||
<option value="48">Freezing precipitation, heavy</option>
|
||||
<option value="49">Reserved</option>
|
||||
<option value="50">Drizzle</option>
|
||||
<option value="51">Drizzle, not freezing, slight</option>
|
||||
<option value="52">Drizzle, not freezing, moderate</option>
|
||||
<option value="53">Drizzle, not freezing, heavy</option>
|
||||
<option value="54">Drizzle, freezing, slight</option>
|
||||
<option value="55">Drizzle, freezing, moderate</option>
|
||||
<option value="56">Drizzle, freezing, heavy</option>
|
||||
<option value="57">Drizzle and rain, slight</option>
|
||||
<option value="58">Drizzle and rain, moderate or heavy</option>
|
||||
<option value="59">Reserved</option>
|
||||
<option value="60">Rain</option>
|
||||
<option value="61">Rain, not freezing, slight</option>
|
||||
<option value="62">Rain, not freezing, moderate</option>
|
||||
<option value="63">Rain, not freezing, heavy</option>
|
||||
<option value="64">Rain, freezing, slight</option>
|
||||
<option value="65">Rain, freezing, moderate</option>
|
||||
<option value="66">Rain, freezing, heavy</option>
|
||||
<option value="67">Rain (or drizzle) and snow, slight</option>
|
||||
<option value="68">Rain (or drizzle) and snow, moderate or heavy</option>
|
||||
<option value="69">Reserved</option>
|
||||
<option value="70">Snow</option>
|
||||
<option value="71">Snow, slight</option>
|
||||
<option value="72">Snow, moderate</option>
|
||||
<option value="73">Snow, heavy</option>
|
||||
<option value="74">Ice pellets, slight</option>
|
||||
<option value="75">Ice pellets, moderate</option>
|
||||
<option value="76">Ice pellets, heavy</option>
|
||||
<option value="77">Snow grains</option>
|
||||
<option value="78">Ice crystals</option>
|
||||
<option value="79">Reserved</option>
|
||||
<option value="80">Shower(s) or intermittent precipitation</option>
|
||||
<option value="81">Rain shower(s) or intermittent rain, slight</option>
|
||||
<option value="82">Rain shower(s) or intermittent rain, moderate</option>
|
||||
<option value="83">Rain shower(s) or intermittent rain, heavy</option>
|
||||
<option value="84">Rain shower(s) or intermittent rain, violent</option>
|
||||
<option value="85">Snow shower(s) or intermittent snow, slight</option>
|
||||
<option value="86">Snow shower(s) or intermittent snow, moderate</option>
|
||||
<option value="87">Snow shower(s) or intermittent snow, heavy</option>
|
||||
<option value="88">Reserved</option>
|
||||
<option value="89">Hail</option>
|
||||
<option value="90">Thunderstorm</option>
|
||||
<option value="91">Thunderstorm, slight or moderate, with no precipitation</option>
|
||||
<option value="92">Thunderstorm, slight or moderate, with rain showers and/or snow showers</option>
|
||||
<option value="93">Thunderstorm, slight or moderate, with hail</option>
|
||||
<option value="94">Thunderstorm, heavy, with no precipitation</option>
|
||||
<option value="95">Thunderstorm, heavy, with rain showers and/or snow showers</option>
|
||||
<option value="96">Thunderstorm, heavy, with hail</option>
|
||||
<!-- 97-98: Reserved -->
|
||||
<option value="99">Tornado</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
<channel-type id="total-cloud-cover-channel">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Total Cloud Cover</label>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
<channel-type id="precipitation-intensity-channel">
|
||||
<item-type>Number:Speed</item-type>
|
||||
<label>Precipitation Intensity</label>
|
||||
<description>Equivalent to the precipitation amount if the same intensity prevails for an hour. </description>
|
||||
<state readOnly="true" pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="weather-id-channel">
|
||||
<item-type>Number</item-type>
|
||||
<label>Prevailing Weather Id</label>
|
||||
<description>Prevailing weather code (WeatherSymbol3 field). For Finnish explanation, consult
|
||||
https://www.ilmatieteenlaitos.fi/latauspalvelun-pikaohje</description>
|
||||
<state readOnly="true" min="1" max="177" pattern="%s">
|
||||
<options>
|
||||
<!-- This corresponds to WeatherSymbol3 -->
|
||||
<!-- Finnish descriptions: https://ilmatieteenlaitos.fi/latauspalvelun-pikaohje -->
|
||||
<option value="1">clear</option>
|
||||
<option value="2">partly cloudy</option>
|
||||
<option value="21">light showers</option>
|
||||
<option value="22">moderate showers</option>
|
||||
<option value="23">heavy showers</option>
|
||||
<option value="3">cloudy</option>
|
||||
<option value="31">light rain</option>
|
||||
<option value="32">moderate rain</option>
|
||||
<option value="33">heavy rain</option>
|
||||
<option value="41">light snow showers</option>
|
||||
<option value="42">moderate snow showers</option>
|
||||
<option value="43">heavy snow showers</option>
|
||||
<option value="51">light snowfall</option>
|
||||
<option value="52">moderate snowfall</option>
|
||||
<option value="53">heavy snowfall</option>
|
||||
<option value="61">thundershowers</option>
|
||||
<option value="62">heavy thundershowers</option>
|
||||
<option value="63">thunder</option>
|
||||
<option value="64">heavy thunder</option>
|
||||
<option value="71">light sleet showers</option>
|
||||
<option value="72">moderate sleet showers</option>
|
||||
<option value="73">heavy sleet showers</option>
|
||||
<option value="81">light sleet</option>
|
||||
<option value="82">moderate sleet</option>
|
||||
<option value="83">heavy sleet</option>
|
||||
<option value="91">haze</option>
|
||||
<option value="92">fog</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.junit.Before;
|
||||
import org.openhab.binding.fmiweather.internal.client.Client;
|
||||
import org.openhab.binding.fmiweather.internal.client.Data;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMIResponse;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
|
||||
/**
|
||||
* Base class for response parsing tests
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class AbstractFMIResponseParsingTest {
|
||||
|
||||
@NonNullByDefault({})
|
||||
protected Client client;
|
||||
|
||||
@Before
|
||||
public void setUpClient() {
|
||||
client = new Client();
|
||||
}
|
||||
|
||||
protected Path getTestResource(String filename) {
|
||||
try {
|
||||
return Paths.get(getClass().getResource(filename).toURI());
|
||||
} catch (URISyntaxException e) {
|
||||
fail(e.getMessage());
|
||||
// Make the compiler happy by throwing here, fails already above
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
protected String readTestResourceUtf8(String filename) {
|
||||
return readTestResourceUtf8(getTestResource(filename));
|
||||
}
|
||||
|
||||
protected String readTestResourceUtf8(Path path) {
|
||||
try {
|
||||
BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8);
|
||||
StringBuilder content = new StringBuilder();
|
||||
char[] buffer = new char[1024];
|
||||
int read = -1;
|
||||
while ((read = reader.read(buffer)) != -1) {
|
||||
content.append(buffer, 0, read);
|
||||
}
|
||||
return content.toString();
|
||||
} catch (IOException e) {
|
||||
fail(e.getMessage());
|
||||
// Make the compiler happy by throwing here, fails already above
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
protected static TypeSafeMatcher<Location> deeplyEqualTo(Location location) {
|
||||
return new ResponseLocationMatcher(location);
|
||||
}
|
||||
|
||||
protected static Matcher<Data> deeplyEqualTo(long start, int intervalMinutes, String... values) {
|
||||
return new TypeSafeMatcher<Data>() {
|
||||
|
||||
private TimestampMatcher timestampMatcher = new TimestampMatcher(start, intervalMinutes, values.length);
|
||||
private ValuesMatcher valuesMatcher = new ValuesMatcher(values);
|
||||
|
||||
@Override
|
||||
public void describeTo(@Nullable Description description) {
|
||||
if (description == null) {
|
||||
return;
|
||||
}
|
||||
description.appendDescriptionOf(timestampMatcher);
|
||||
description.appendText(" and ");
|
||||
description.appendDescriptionOf(valuesMatcher);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(Data dataValues) {
|
||||
return timestampMatcher.matches(dataValues.timestampsEpochSecs)
|
||||
&& valuesMatcher.matches(dataValues.values);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param content
|
||||
* @return
|
||||
* @throws Throwable exception raised by parseMultiPointCoverageXml
|
||||
* @throws AssertionError exception raised when parseMultiPointCoverageXml method signature does not match excepted
|
||||
* (test & implementation is out-of-sync)
|
||||
*/
|
||||
protected FMIResponse parseMultiPointCoverageXml(String content) throws Throwable {
|
||||
try {
|
||||
Method parseMethod = Client.class.getDeclaredMethod("parseMultiPointCoverageXml", String.class);
|
||||
parseMethod.setAccessible(true);
|
||||
return (FMIResponse) parseMethod.invoke(client, content);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw e.getTargetException();
|
||||
} catch (Exception e) {
|
||||
fail(String.format("Unexpected reflection error (code changed?) %s: %s", e.getClass().getName(),
|
||||
e.getMessage()));
|
||||
// Make the compiler happy by throwing here, fails already above
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected Set<Location> parseStations(String content) {
|
||||
try {
|
||||
Method parseMethod = Client.class.getDeclaredMethod("parseStations", String.class);
|
||||
parseMethod.setAccessible(true);
|
||||
return (Set<Location>) parseMethod.invoke(client, content);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getTargetException());
|
||||
} catch (Exception e) {
|
||||
fail(String.format("Unexpected reflection error (code changed?) %s: %s", e.getClass().getName(),
|
||||
e.getMessage()));
|
||||
// Make the compiler happy by throwing here, fails already above
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMISID;
|
||||
import org.openhab.binding.fmiweather.internal.client.ForecastRequest;
|
||||
import org.openhab.binding.fmiweather.internal.client.LatLon;
|
||||
import org.openhab.binding.fmiweather.internal.client.ObservationRequest;
|
||||
import org.openhab.binding.fmiweather.internal.client.QueryParameter;
|
||||
|
||||
/**
|
||||
* Tests for converting Request objects to URLs
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIRequestTest {
|
||||
|
||||
@Test
|
||||
public void testObservationRequestToUrl() {
|
||||
ObservationRequest request = new ObservationRequest(new FMISID("101023"), 1552215664L, 1552215665L, 61);
|
||||
assertThat(request.toUrl(), is(
|
||||
"https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::observations::weather::multipointcoverage"
|
||||
+ "&starttime=2019-03-10T11:01:04Z&endtime=2019-03-10T11:01:05Z×tep=61&fmisid=101023"
|
||||
+ "¶meters=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea,r_1h,snow_aws,vis,n_man,wawa"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForecastRequestToUrl() {
|
||||
ForecastRequest request = new ForecastRequest(new LatLon(new BigDecimal("9"), new BigDecimal("8")), 1552215664L,
|
||||
1552215665L, 61);
|
||||
assertThat(request.toUrl(), is(
|
||||
"https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::hirlam::surface::point::multipointcoverage"
|
||||
+ "&starttime=2019-03-10T11:01:04Z&endtime=2019-03-10T11:01:05Z×tep=61&latlon=9,8"
|
||||
+ "¶meters=Temperature,Humidity,WindDirection,WindSpeedMS,WindGust,Pressure,Precipitation1h,TotalCloudCover,WeatherSymbol3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomLocation() {
|
||||
QueryParameter location = new QueryParameter() {
|
||||
|
||||
@Override
|
||||
public List<Entry<String, String>> toRequestParameters() {
|
||||
return Arrays.asList(new AbstractMap.SimpleImmutableEntry<>("lat", "MYLAT"),
|
||||
new AbstractMap.SimpleImmutableEntry<>("lon", "FOO"),
|
||||
new AbstractMap.SimpleImmutableEntry<>("special", "x,y,z"));
|
||||
}
|
||||
};
|
||||
ObservationRequest request = new ObservationRequest(location, 1552215664L, 1552215665L, 61);
|
||||
assertThat(request.toUrl(), is(
|
||||
"https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::observations::weather::multipointcoverage"
|
||||
+ "&starttime=2019-03-10T11:01:04Z&endtime=2019-03-10T11:01:05Z×tep=61&lat=MYLAT&lon=FOO&special=x,y,z"
|
||||
+ "¶meters=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea,r_1h,snow_aws,vis,n_man,wawa"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.fmiweather.internal.client.Client;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMIResponse;
|
||||
|
||||
/**
|
||||
* Test cases for {@link Client.parseMultiPointCoverageXml} with an "empty" (no data) XML response
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
public class FMIResponseParsingEmptyTest extends AbstractFMIResponseParsingTest {
|
||||
|
||||
private Path observations = getTestResource("observations_empty.xml");
|
||||
|
||||
private FMIResponse observationsResponse;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
client = new Client();
|
||||
try {
|
||||
observationsResponse = parseMultiPointCoverageXml(readTestResourceUtf8(observations));
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Test data malformed", e);
|
||||
}
|
||||
assertNotNull(observationsResponse);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationsSinglePlace() throws Throwable {
|
||||
assertThat(observationsResponse.getLocations().size(), is(0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIResponseException;
|
||||
|
||||
/**
|
||||
* Test cases for AbstractWeatherHandler. The tests provide mocks for supporting entities using Mockito.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIResponseParsingExceptionReportTest extends AbstractFMIResponseParsingTest {
|
||||
|
||||
private Path error1 = getTestResource("error1.xml");
|
||||
|
||||
@Test
|
||||
public void testErrorResponse() {
|
||||
try {
|
||||
parseMultiPointCoverageXml(readTestResourceUtf8(error1));
|
||||
} catch (FMIResponseException e) {
|
||||
// OK
|
||||
assertThat(e.getMessage(), is(
|
||||
"Exception report (OperationParsingFailed): [Invalid time interval!, The start time is later than the end time., URI:\n\t\t\t/wfs?endtime=1900-03-10T20%3A10%3A00Z&fmisid=101023¶meters=t2m%2Crh%2Cwd_10min%2Cws_10min%2Cwg_10min%2Cp_sea&request=getFeature&service=WFS&starttime=2019-03-10T10%3A10%3A00Z&storedquery_id=fmi%3A%3Aobservations%3A%3Aweather%3A%3Amultipointcoverage×tep=60&version=2.0.0]"));
|
||||
return;
|
||||
} catch (Throwable e) {
|
||||
fail("Wrong exception, was " + e.getClass().getName());
|
||||
}
|
||||
fail("FMIResponseException expected");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.fmiweather.internal.client.exception.FMIResponseException;
|
||||
import org.xml.sax.SAXParseException;
|
||||
|
||||
/**
|
||||
* Test cases for AbstractWeatherHandler. The tests provide mocks for supporting entities using Mockito.
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIResponseParsingInvalidOrUnexpectedXmlTest extends AbstractFMIResponseParsingTest {
|
||||
|
||||
private Path observations1 = getTestResource("observations_single_place.xml");
|
||||
|
||||
@Test(expected = SAXParseException.class)
|
||||
public void testInvalidXml() throws IOException, Throwable {
|
||||
parseMultiPointCoverageXml(readTestResourceUtf8(observations1).replace("276.0", "<<"));
|
||||
}
|
||||
|
||||
@Test(expected = FMIResponseException.class)
|
||||
public void testUnexpectedXml() throws IOException, Throwable {
|
||||
parseMultiPointCoverageXml(readTestResourceUtf8(observations1).replace("276.0", "<foo>4</foo>"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.fmiweather.internal.client.Data;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMIResponse;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
|
||||
/**
|
||||
* Test cases for Client.parseMultiPointCoverageXml with a xml response having multiple places, parameters
|
||||
* and timestamps
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIResponseParsingMultiplePlacesTest extends AbstractFMIResponseParsingTest {
|
||||
|
||||
private Path observationsMultiplePlaces = getTestResource("observations_multiple_places.xml");
|
||||
private Path forecastsMultiplePlaces = getTestResource("forecast_multiple_places.xml");
|
||||
|
||||
@NonNullByDefault({})
|
||||
private FMIResponse observationsMultiplePlacesResponse;
|
||||
@NonNullByDefault({})
|
||||
private FMIResponse observationsMultiplePlacesNaNResponse;
|
||||
@NonNullByDefault({})
|
||||
private FMIResponse forecastsMultiplePlacesResponse;
|
||||
|
||||
// observation station points (observations_multiple_places.xml) have fmisid as their id
|
||||
private Location emasalo = new Location("Porvoo Emäsalo", "101023", new BigDecimal("60.20382"),
|
||||
new BigDecimal("25.62546"));
|
||||
private Location kilpilahti = new Location("Porvoo Kilpilahti satama", "100683", new BigDecimal("60.30373"),
|
||||
new BigDecimal("25.54916"));
|
||||
private Location harabacka = new Location("Porvoo Harabacka", "101028", new BigDecimal("60.39172"),
|
||||
new BigDecimal("25.60730"));
|
||||
|
||||
// forecast points (forecast_multiple_places.xml) have latitude,longitude as their id
|
||||
private Location maarianhamina = new Location("Mariehamn", "60.09726,19.93481", new BigDecimal("60.09726"),
|
||||
new BigDecimal("19.93481"));
|
||||
private Location pointWithNoName = new Location("19.9,61.0973", "61.09726,19.90000", new BigDecimal("61.09726"),
|
||||
new BigDecimal("19.90000"));
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
try {
|
||||
observationsMultiplePlacesResponse = parseMultiPointCoverageXml(
|
||||
readTestResourceUtf8(observationsMultiplePlaces));
|
||||
observationsMultiplePlacesNaNResponse = parseMultiPointCoverageXml(
|
||||
readTestResourceUtf8(observationsMultiplePlaces).replace("276.0", "NaN"));
|
||||
forecastsMultiplePlacesResponse = parseMultiPointCoverageXml(readTestResourceUtf8(forecastsMultiplePlaces));
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Test data malformed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testLocationsMultiplePlacesObservations() {
|
||||
// locations
|
||||
assertThat(observationsMultiplePlacesResponse.getLocations().size(), is(3));
|
||||
assertThat(observationsMultiplePlacesResponse.getLocations(),
|
||||
hasItems(deeplyEqualTo(emasalo), deeplyEqualTo(kilpilahti), deeplyEqualTo(harabacka)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testLocationsMultiplePlacesForecasts() {
|
||||
// locations
|
||||
assertThat(forecastsMultiplePlacesResponse.getLocations().size(), is(2));
|
||||
assertThat(forecastsMultiplePlacesResponse.getLocations(),
|
||||
hasItems(deeplyEqualTo(maarianhamina), deeplyEqualTo(pointWithNoName)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParametersMultipleObservations() {
|
||||
for (Location location : new Location[] { emasalo, kilpilahti, harabacka }) {
|
||||
Optional<Set<String>> parametersOptional = observationsMultiplePlacesResponse.getParameters(location);
|
||||
Set<String> parameters = parametersOptional.get();
|
||||
assertThat(parameters.size(), is(6));
|
||||
assertThat(parameters, hasItems("wd_10min", "wg_10min", "rh", "p_sea", "ws_10min", "t2m"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParametersMultipleForecasts() {
|
||||
for (Location location : new Location[] { maarianhamina, pointWithNoName }) {
|
||||
Optional<Set<String>> parametersOptional = forecastsMultiplePlacesResponse.getParameters(location);
|
||||
Set<String> parameters = parametersOptional.get();
|
||||
assertThat(parameters.size(), is(2));
|
||||
assertThat(parameters, hasItems("Temperature", "Humidity"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseObservationsMultipleData() {
|
||||
Data wd_10min = observationsMultiplePlacesResponse.getData(emasalo, "wd_10min").get();
|
||||
assertThat(wd_10min, is(deeplyEqualTo(1552215600L, 60, "312.0", "286.0", "295.0", "282.0", "271.0", "262.0",
|
||||
"243.0", "252.0", "262.0", "276.0")));
|
||||
Data rh = observationsMultiplePlacesResponse.getData(kilpilahti, "rh").get();
|
||||
assertThat(rh, is(deeplyEqualTo(1552215600L, 60, "73.0", "65.0", "60.0", "59.0", "57.0", "64.0", "66.0", "65.0",
|
||||
"71.0", "77.0")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseForecastsMultipleData() {
|
||||
Data temperature = forecastsMultiplePlacesResponse.getData(maarianhamina, "Temperature").get();
|
||||
assertThat(temperature, is(deeplyEqualTo(1553688000, 360, "3.84", "2.62", "2.26", "1.22", "5.47", "5.52",
|
||||
"5.42", "4.78", "8.34", "7.15", null, null, null, null)));
|
||||
Data temperature2 = forecastsMultiplePlacesResponse.getData(pointWithNoName, "Temperature").get();
|
||||
assertThat(temperature2, is(deeplyEqualTo(1553688000, 360, "1.54", "2.91", "2.41", "2.36", "4.22", "5.28",
|
||||
"4.58", "4.0", "4.79", "5.4", null, null, null, null)));
|
||||
|
||||
Data humidity = forecastsMultiplePlacesResponse.getData(maarianhamina, "Humidity").get();
|
||||
assertThat(humidity, is(deeplyEqualTo(1553688000, 360, "66.57", "87.38", "85.77", "96.3", "75.74", "81.7",
|
||||
"86.78", "87.96", "70.86", "76.35", null, null, null, null)));
|
||||
Data humidity2 = forecastsMultiplePlacesResponse.getData(pointWithNoName, "Humidity").get();
|
||||
assertThat(humidity2, is(deeplyEqualTo(1553688000, 360, "90.18", "86.22", "89.18", "89.43", "77.26", "78.55",
|
||||
"83.36", "85.83", "80.82", "76.92", null, null, null, null)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseObservations1NaN() {
|
||||
// last value is null, due to NaN measurement value
|
||||
Data wd_10min = observationsMultiplePlacesNaNResponse.getData(emasalo, "wd_10min").get();
|
||||
assertThat(wd_10min, is(deeplyEqualTo(1552215600L, 60, "312.0", "286.0", "295.0", "282.0", "271.0", "262.0",
|
||||
"243.0", "252.0", "262.0", null)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.fmiweather.internal.client.Data;
|
||||
import org.openhab.binding.fmiweather.internal.client.FMIResponse;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
|
||||
/**
|
||||
* Test cases for Client.parseMultiPointCoverageXml with a xml response having single place and multiple
|
||||
* parameters
|
||||
* and timestamps
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class FMIResponseParsingSinglePlaceTest extends AbstractFMIResponseParsingTest {
|
||||
|
||||
private Path observations1 = getTestResource("observations_single_place.xml");
|
||||
|
||||
@NonNullByDefault({})
|
||||
private FMIResponse observationsResponse1;
|
||||
@NonNullByDefault({})
|
||||
private FMIResponse observationsResponse1NaN;
|
||||
private Location emasalo = new Location("Porvoo Emäsalo", "101023", new BigDecimal("60.20382"),
|
||||
new BigDecimal("25.62546"));
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
try {
|
||||
observationsResponse1 = parseMultiPointCoverageXml(readTestResourceUtf8(observations1));
|
||||
observationsResponse1NaN = parseMultiPointCoverageXml(
|
||||
readTestResourceUtf8(observations1).replace("276.0", "NaN"));
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Test data malformed", e);
|
||||
}
|
||||
assertNotNull(observationsResponse1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationsSinglePlace() {
|
||||
assertThat(observationsResponse1.getLocations().size(), is(1));
|
||||
assertThat(observationsResponse1.getLocations().stream().findFirst().get(), deeplyEqualTo(emasalo));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParameters() {
|
||||
// parameters
|
||||
Optional<Set<String>> parametersOptional = observationsResponse1.getParameters(emasalo);
|
||||
Set<String> parameters = parametersOptional.get();
|
||||
assertThat(parameters.size(), is(6));
|
||||
assertThat(parameters, hasItems("wd_10min", "wg_10min", "rh", "p_sea", "ws_10min", "t2m"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetDataWithInvalidArguments() {
|
||||
Location loc = observationsResponse1.getLocations().stream().findAny().get();
|
||||
// Invalid parameter or location (fmisid)
|
||||
assertThat(observationsResponse1.getData(loc, "foobar"), is(Optional.empty()));
|
||||
assertThat(observationsResponse1.getData(
|
||||
new Location("Porvoo Emäsalo", "9999999", new BigDecimal("60.20382"), new BigDecimal("25.62546")),
|
||||
"rh"), is(Optional.empty()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseObservations1Data() {
|
||||
Data wd_10min = observationsResponse1.getData(emasalo, "wd_10min").get();
|
||||
assertThat(wd_10min, is(deeplyEqualTo(1552215600L, 60, "312.0", "286.0", "295.0", "282.0", "271.0", "262.0",
|
||||
"243.0", "252.0", "262.0", "276.0")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testParseObservations1NaN() {
|
||||
// last value is null, due to NaN measurement value
|
||||
Data wd_10min = observationsResponse1NaN.getData(emasalo, "wd_10min").get();
|
||||
assertThat(wd_10min, is(deeplyEqualTo(1552215600L, 60, "312.0", "286.0", "295.0", "282.0", "271.0", "262.0",
|
||||
"243.0", "252.0", "262.0", null)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.junit.Test;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
|
||||
/**
|
||||
* Test cases for Client.parseStations
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ParsingStationsTest extends AbstractFMIResponseParsingTest {
|
||||
|
||||
private Path stations_xml = getTestResource("stations.xml");
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void testParseStations() {
|
||||
Set<Location> stations = parseStations(readTestResourceUtf8(stations_xml));
|
||||
assertNotNull(stations);
|
||||
assertThat(stations.size(), is(3));
|
||||
assertThat(stations,
|
||||
hasItems(
|
||||
deeplyEqualTo(new Location("Porvoo Kilpilahti satama", "100683", new BigDecimal("60.303725"),
|
||||
new BigDecimal("25.549164"))),
|
||||
deeplyEqualTo(new Location("Parainen Utö", "100908", new BigDecimal("59.779094"),
|
||||
new BigDecimal("21.374788"))),
|
||||
deeplyEqualTo(new Location("Lemland Nyhamn", "100909", new BigDecimal("59.959108"),
|
||||
new BigDecimal("19.953736")))));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
import org.openhab.binding.fmiweather.internal.client.Location;
|
||||
|
||||
/**
|
||||
* Hamcrest matcher for Location objects
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ResponseLocationMatcher extends TypeSafeMatcher<Location> {
|
||||
|
||||
public final String name;
|
||||
public final String id;
|
||||
@Nullable
|
||||
public final BigDecimal latitude;
|
||||
@Nullable
|
||||
public final BigDecimal longitude;
|
||||
|
||||
public ResponseLocationMatcher(Location template) {
|
||||
this(template.name, template.id, template.latitude, template.longitude);
|
||||
}
|
||||
|
||||
public ResponseLocationMatcher(String name, String id, @Nullable String latitude, @Nullable String longitude) {
|
||||
this(name, id, latitude == null ? null : new BigDecimal(latitude),
|
||||
longitude == null ? null : new BigDecimal(longitude));
|
||||
}
|
||||
|
||||
public ResponseLocationMatcher(String name, String id, @Nullable BigDecimal latitude,
|
||||
@Nullable BigDecimal longitude) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.id = id;
|
||||
this.latitude = latitude;
|
||||
this.longitude = longitude;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(@Nullable Description description) {
|
||||
if (description == null) {
|
||||
return;
|
||||
}
|
||||
description.appendText("Location(name=\"").appendText(name).appendText("\", id=\"").appendText(id)
|
||||
.appendText("\", latitude=").appendText(Objects.toString(latitude)).appendText(", longitude=")
|
||||
.appendText(Objects.toString(longitude)).appendText(")");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(Location loc) {
|
||||
return Objects.deepEquals(name, loc.name) && Objects.deepEquals(id, loc.id)
|
||||
&& Objects.deepEquals(latitude, loc.latitude) && Objects.deepEquals(longitude, loc.longitude);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
/**
|
||||
* Hamcrest matcher for timestamps
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class TimestampMatcher extends TypeSafeMatcher<long[]> {
|
||||
|
||||
private long start;
|
||||
private int intervalMinutes;
|
||||
private long steps;
|
||||
|
||||
public TimestampMatcher(long start, int intervalMinutes, long steps) {
|
||||
this.start = start;
|
||||
this.intervalMinutes = intervalMinutes;
|
||||
this.steps = steps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(@Nullable Description description) {
|
||||
if (description == null) {
|
||||
return;
|
||||
}
|
||||
description.appendText(new StringBuilder("start=").append(start).append(", length=").append(steps)
|
||||
.append(", interval=").append(intervalMinutes).toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(long[] timestamps) {
|
||||
return verifyLength(timestamps) && verifyStart(timestamps) && verifyStep(timestamps);
|
||||
}
|
||||
|
||||
private boolean verifyLength(long[] timestamps) {
|
||||
return timestamps.length == steps;
|
||||
}
|
||||
|
||||
private boolean verifyStart(long[] timestamps) {
|
||||
return timestamps[0] == start;
|
||||
}
|
||||
|
||||
private boolean verifyStep(long[] timestamps) {
|
||||
for (int i = 1; i < timestamps.length; i++) {
|
||||
if (timestamps[i] - timestamps[i - 1] != intervalMinutes * 60) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2010-2020 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.fmiweather;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
/**
|
||||
* Hamcrest matcher for values
|
||||
*
|
||||
* @author Sami Salonen - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class ValuesMatcher extends TypeSafeMatcher<@Nullable BigDecimal[]> {
|
||||
|
||||
private Object[] values;
|
||||
|
||||
public ValuesMatcher(@Nullable String... values) {
|
||||
this.values = Stream.of(values).map(s -> stringToBigDecimal(s)).toArray();
|
||||
}
|
||||
|
||||
private static @Nullable BigDecimal stringToBigDecimal(@Nullable String s) {
|
||||
return s == null ? null : new BigDecimal(s);
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Override
|
||||
public void describeTo(@Nullable Description description) {
|
||||
if (description == null) {
|
||||
return;
|
||||
}
|
||||
description.appendText(Arrays.deepToString(values));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(@Nullable BigDecimal[] data) {
|
||||
return Objects.deepEquals(data, values);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::observations::weather::multipointcoverage&fmisid=101023&starttime=2019-03-10T10:10:00Z&endtime=1900-03-10T20:10:00Z×tep=60¶meters=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea -->
|
||||
<ExceptionReport xmlns="http://www.opengis.net/ows/1.1"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.opengis.net/ows/1.1 http://schemas.opengis.net/ows/1.1.0/owsExceptionReport.xsd"
|
||||
version="2.0.0" xml:lang="eng">
|
||||
|
||||
|
||||
<Exception exceptionCode="OperationParsingFailed">
|
||||
<ExceptionText>Invalid time interval!</ExceptionText>
|
||||
<ExceptionText>The start time is later than the end time.</ExceptionText>
|
||||
<ExceptionText>URI:
|
||||
/wfs?endtime=1900-03-10T20%3A10%3A00Z&fmisid=101023&parameters=t2m%2Crh%2Cwd_10min%2Cws_10min%2Cwg_10min%2Cp_sea&request=getFeature&service=WFS&starttime=2019-03-10T10%3A10%3A00Z&storedquery_id=fmi%3A%3Aobservations%3A%3Aweather%3A%3Amultipointcoverage&timestep=60&version=2.0.0</ExceptionText>
|
||||
|
||||
</Exception>
|
||||
|
||||
</ExceptionReport>
|
||||
@@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::forecast::hirlam::surface::point::multipointcoverage&latlon=60.09726,19.93481&latlon=61.09726,19.9&starttime=2019-03-27T10:10:00Z&endtime=2019-03-30T20:10:00Z×tep=360¶meters=Temperature,Humidity -->
|
||||
<wfs:FeatureCollection timeStamp="2019-03-27T19:46:01Z" numberMatched="1" numberReturned="1"
|
||||
xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:om="http://www.opengis.net/om/2.0"
|
||||
xmlns:omso="http://inspire.ec.europa.eu/schemas/omso/3.0" xmlns:ompr="http://inspire.ec.europa.eu/schemas/ompr/3.0"
|
||||
xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:gmd="http://www.isotc211.org/2005/gmd"
|
||||
xmlns:gco="http://www.isotc211.org/2005/gco" xmlns:swe="http://www.opengis.net/swe/2.0"
|
||||
xmlns:gmlcov="http://www.opengis.net/gmlcov/1.0" xmlns:sam="http://www.opengis.net/sampling/2.0"
|
||||
xmlns:sams="http://www.opengis.net/samplingSpatial/2.0"
|
||||
xmlns:target="http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0"
|
||||
xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd
|
||||
http://www.opengis.net/gmlcov/1.0 http://schemas.opengis.net/gmlcov/1.0/gmlcovAll.xsd
|
||||
http://www.opengis.net/sampling/2.0 http://schemas.opengis.net/sampling/2.0/samplingFeature.xsd
|
||||
http://www.opengis.net/samplingSpatial/2.0 http://schemas.opengis.net/samplingSpatial/2.0/spatialSamplingFeature.xsd
|
||||
http://www.opengis.net/swe/2.0 http://schemas.opengis.net/sweCommon/2.0/swe.xsd
|
||||
http://inspire.ec.europa.eu/schemas/omso/3.0 http://inspire.ec.europa.eu/schemas/omso/3.0/SpecialisedObservations.xsd
|
||||
http://inspire.ec.europa.eu/schemas/ompr/3.0 http://inspire.ec.europa.eu/schemas/ompr/3.0/Processes.xsd
|
||||
http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0 http://xml.fmi.fi/schema/om/atmosphericfeatures/1.0/atmosphericfeatures.xsd">
|
||||
|
||||
<wfs:member>
|
||||
<omso:GridSeriesObservation
|
||||
gml:id="WFS-QGPHvQmfZT9VhmkXx4WpnRrbPFuJTowuYWbbpdOs2_llx4efR060aeWzDtdOufXlmw48rp1w36d3R0629dnTTw36d3THv7ZeWHPlhaWLLn07qmnbltR_wo3bxvHCY2PlzrUi0Kcd06aMmrhnZd2Spp25bUf8KN88eTRHBm07sk7Lh5ZefSth2ackhmZ8u_Tk51nM2DRi3Zsqxp2Gc6NeXz338sl_f2y8u_LT0w4tmWJpbMvbLsqeeGWpmbN.PDsy1qZtN.NJXdemZw1tuHxE08.mHdjy0rV0IDW26efPTuz1MvjpWNOwzmVt35MuyszRp5bMO1lcMPLDtrWqZdvDLyw9OvLLWhI67dOTT08tzn038suTj1y8vN_TkrzCzbdLp1m38suPDz6OnWjTy2Ydrp1z68s2HHldOuG_Tu6OnW3rs6aeG_Tu6Y9_bLyw58rQ6aduWn0y8J0Vmh007ctrfuy1jVakMA--">
|
||||
<om:phenomenonTime>
|
||||
<gml:TimePeriod gml:id="time-interval-1-1">
|
||||
<gml:beginPosition>2019-03-27T12:00:00Z</gml:beginPosition>
|
||||
<gml:endPosition>2019-03-30T18:00:00Z</gml:endPosition>
|
||||
</gml:TimePeriod>
|
||||
</om:phenomenonTime>
|
||||
<om:resultTime>
|
||||
<gml:TimeInstant gml:id="time-1-1">
|
||||
<gml:timePosition>2019-03-27T15:19:43Z</gml:timePosition>
|
||||
</gml:TimeInstant>
|
||||
</om:resultTime>
|
||||
|
||||
<om:procedure xlink:href="http://xml.fmi.fi/inspire/process/hirlam" />
|
||||
<om:parameter>
|
||||
<om:NamedValue>
|
||||
<om:name xlink:href="http://xml.fmi.fi/inspire/process/hirlam" />
|
||||
<om:value>
|
||||
<gml:TimeInstant gml:id="analysis-time-1-1">
|
||||
<gml:timePosition>2019-03-27T12:00:00Z</gml:timePosition>
|
||||
</gml:TimeInstant>
|
||||
</om:value>
|
||||
</om:NamedValue>
|
||||
</om:parameter>
|
||||
<om:observedProperty
|
||||
xlink:href="http://opendata.fmi.fi/meta?observableProperty=forecast&param=Temperature,Humidity&language=eng" />
|
||||
<om:featureOfInterest>
|
||||
<sams:SF_SpatialSamplingFeature gml:id="enn-s-1-1-">
|
||||
<sam:sampledFeature>
|
||||
<target:LocationCollection gml:id="sampled-target-1-1">
|
||||
<target:member>
|
||||
<target:Location gml:id="forloc-geoid-3041732-pos">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/geoid">3041732</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">Mariehamn</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">3041732</gml:name>
|
||||
<target:representativePoint xlink:href="#point-3041732" />
|
||||
<target:country codeSpace="http://xml.fmi.fi/namespace/location/country">Finland</target:country>
|
||||
<target:timezone>Europe/Mariehamn</target:timezone>
|
||||
<target:region codeSpace="http://xml.fmi.fi/namespace/location/region">Maarianhamina</target:region>
|
||||
</target:Location>
|
||||
</target:member>
|
||||
<target:member>
|
||||
<target:Location gml:id="forloc-geoid-NaN-pos">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/geoid">NaN</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">19.9,61.0973</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">NaN</gml:name>
|
||||
<target:representativePoint xlink:href="#point-NaN" />
|
||||
<target:country codeSpace="http://xml.fmi.fi/namespace/location/country"></target:country>
|
||||
<target:timezone>Europe/Helsinki</target:timezone>
|
||||
|
||||
</target:Location>
|
||||
</target:member>
|
||||
</target:LocationCollection>
|
||||
</sam:sampledFeature>
|
||||
<sams:shape>
|
||||
<gml:MultiPoint gml:id="sf-1-1-">
|
||||
<gml:pointMembers>
|
||||
<gml:Point gml:id="point-3041732" srsName="http://www.opengis.net/def/crs/EPSG/0/4258"
|
||||
srsDimension="2">
|
||||
<gml:name>Mariehamn</gml:name>
|
||||
<gml:pos>60.09726 19.93481 </gml:pos>
|
||||
</gml:Point>
|
||||
<gml:Point gml:id="point-NaN" srsName="http://www.opengis.net/def/crs/EPSG/0/4258" srsDimension="2">
|
||||
<gml:name>19.9,61.0973</gml:name>
|
||||
<gml:pos>61.09726 19.90000 </gml:pos>
|
||||
</gml:Point>
|
||||
</gml:pointMembers>
|
||||
</gml:MultiPoint>
|
||||
</sams:shape>
|
||||
</sams:SF_SpatialSamplingFeature>
|
||||
</om:featureOfInterest>
|
||||
<om:result>
|
||||
<gmlcov:MultiPointCoverage gml:id="mpcv-1-1">
|
||||
<gml:domainSet>
|
||||
<gmlcov:SimpleMultiPoint gml:id="mp-1-1"
|
||||
srsName="http://xml.fmi.fi/gml/crs/compoundCRS.php?crs=4258&time=unixtime" srsDimension="3">
|
||||
<gmlcov:positions>
|
||||
60.09726 19.93481 1553688000
|
||||
60.09726 19.93481 1553709600
|
||||
60.09726 19.93481 1553731200
|
||||
60.09726 19.93481 1553752800
|
||||
60.09726 19.93481 1553774400
|
||||
60.09726 19.93481 1553796000
|
||||
60.09726 19.93481 1553817600
|
||||
60.09726 19.93481 1553839200
|
||||
60.09726 19.93481 1553860800
|
||||
60.09726 19.93481 1553882400
|
||||
60.09726 19.93481 1553904000
|
||||
60.09726 19.93481 1553925600
|
||||
60.09726 19.93481 1553947200
|
||||
60.09726 19.93481 1553968800
|
||||
61.09726 19.90000 1553688000
|
||||
61.09726 19.90000 1553709600
|
||||
61.09726 19.90000 1553731200
|
||||
61.09726 19.90000 1553752800
|
||||
61.09726 19.90000 1553774400
|
||||
61.09726 19.90000 1553796000
|
||||
61.09726 19.90000 1553817600
|
||||
61.09726 19.90000 1553839200
|
||||
61.09726 19.90000 1553860800
|
||||
61.09726 19.90000 1553882400
|
||||
61.09726 19.90000 1553904000
|
||||
61.09726 19.90000 1553925600
|
||||
61.09726 19.90000 1553947200
|
||||
61.09726 19.90000 1553968800
|
||||
</gmlcov:positions>
|
||||
</gmlcov:SimpleMultiPoint>
|
||||
</gml:domainSet>
|
||||
<gml:rangeSet>
|
||||
<gml:DataBlock>
|
||||
<gml:rangeParameters />
|
||||
<gml:doubleOrNilReasonTupleList>
|
||||
3.84 66.57
|
||||
2.62 87.38
|
||||
2.26 85.77
|
||||
1.22 96.3
|
||||
5.47 75.74
|
||||
5.52 81.7
|
||||
5.42 86.78
|
||||
4.78 87.96
|
||||
8.34 70.86
|
||||
7.15 76.35
|
||||
NaN NaN
|
||||
NaN NaN
|
||||
NaN NaN
|
||||
NaN NaN
|
||||
1.54 90.18
|
||||
2.91 86.22
|
||||
2.41 89.18
|
||||
2.36 89.43
|
||||
4.22 77.26
|
||||
5.28 78.55
|
||||
4.58 83.36
|
||||
4.0 85.83
|
||||
4.79 80.82
|
||||
5.4 76.92
|
||||
NaN NaN
|
||||
NaN NaN
|
||||
NaN NaN
|
||||
NaN NaN
|
||||
</gml:doubleOrNilReasonTupleList>
|
||||
</gml:DataBlock>
|
||||
</gml:rangeSet>
|
||||
<gml:coverageFunction>
|
||||
<gml:CoverageMappingRule>
|
||||
<gml:ruleDefinition>Linear</gml:ruleDefinition>
|
||||
</gml:CoverageMappingRule>
|
||||
</gml:coverageFunction>
|
||||
<gmlcov:rangeType>
|
||||
<swe:DataRecord>
|
||||
<swe:field name="Temperature"
|
||||
xlink:href="http://opendata.fmi.fi/meta?observableProperty=forecast&param=Temperature&language=eng" />
|
||||
<swe:field name="Humidity"
|
||||
xlink:href="http://opendata.fmi.fi/meta?observableProperty=forecast&param=Humidity&language=eng" />
|
||||
</swe:DataRecord>
|
||||
</gmlcov:rangeType>
|
||||
</gmlcov:MultiPointCoverage>
|
||||
</om:result>
|
||||
|
||||
</omso:GridSeriesObservation>
|
||||
</wfs:member>
|
||||
|
||||
</wfs:FeatureCollection>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::observations::weather::multipointcoverage&fmisid=101023&starttime=2019-03-10T10:10:00Z&endtime=2019-03-10T10:10:00Z×tep=60¶meters=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea -->
|
||||
<wfs:FeatureCollection timeStamp="2019-03-23T08:13:23Z" numberMatched="0" numberReturned="0"
|
||||
xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:om="http://www.opengis.net/om/2.0"
|
||||
xmlns:ompr="http://inspire.ec.europa.eu/schemas/ompr/3.0" xmlns:omso="http://inspire.ec.europa.eu/schemas/omso/3.0"
|
||||
xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:gmd="http://www.isotc211.org/2005/gmd"
|
||||
xmlns:gco="http://www.isotc211.org/2005/gco" xmlns:swe="http://www.opengis.net/swe/2.0"
|
||||
xmlns:gmlcov="http://www.opengis.net/gmlcov/1.0" xmlns:sam="http://www.opengis.net/sampling/2.0"
|
||||
xmlns:sams="http://www.opengis.net/samplingSpatial/2.0"
|
||||
xmlns:target="http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0"
|
||||
xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd
|
||||
http://www.opengis.net/gmlcov/1.0 http://schemas.opengis.net/gmlcov/1.0/gmlcovAll.xsd
|
||||
http://www.opengis.net/sampling/2.0 http://schemas.opengis.net/sampling/2.0/samplingFeature.xsd
|
||||
http://www.opengis.net/samplingSpatial/2.0 http://schemas.opengis.net/samplingSpatial/2.0/spatialSamplingFeature.xsd
|
||||
http://www.opengis.net/swe/2.0 http://schemas.opengis.net/sweCommon/2.0/swe.xsd
|
||||
http://inspire.ec.europa.eu/schemas/ompr/3.0 http://inspire.ec.europa.eu/schemas/ompr/3.0/Processes.xsd
|
||||
http://inspire.ec.europa.eu/schemas/omso/3.0 http://inspire.ec.europa.eu/schemas/omso/3.0/SpecialisedObservations.xsd
|
||||
http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0 http://xml.fmi.fi/schema/om/atmosphericfeatures/1.0/atmosphericfeatures.xsd">
|
||||
|
||||
</wfs:FeatureCollection>
|
||||
@@ -0,0 +1,225 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- multiple locations using place=xxx&maxlocations=3 -->
|
||||
<!-- https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::observations::weather::multipointcoverage&place=porvoo&starttime=2019-03-10T10:10:00Z&endtime=2019-03-10T20:10:00Z×tep=60¶meters=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea&maxlocations=3 -->
|
||||
<wfs:FeatureCollection timeStamp="2019-03-22T21:17:09Z" numberMatched="1" numberReturned="1"
|
||||
xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:om="http://www.opengis.net/om/2.0"
|
||||
xmlns:ompr="http://inspire.ec.europa.eu/schemas/ompr/3.0" xmlns:omso="http://inspire.ec.europa.eu/schemas/omso/3.0"
|
||||
xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:gmd="http://www.isotc211.org/2005/gmd"
|
||||
xmlns:gco="http://www.isotc211.org/2005/gco" xmlns:swe="http://www.opengis.net/swe/2.0"
|
||||
xmlns:gmlcov="http://www.opengis.net/gmlcov/1.0" xmlns:sam="http://www.opengis.net/sampling/2.0"
|
||||
xmlns:sams="http://www.opengis.net/samplingSpatial/2.0"
|
||||
xmlns:target="http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0"
|
||||
xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd
|
||||
http://www.opengis.net/gmlcov/1.0 http://schemas.opengis.net/gmlcov/1.0/gmlcovAll.xsd
|
||||
http://www.opengis.net/sampling/2.0 http://schemas.opengis.net/sampling/2.0/samplingFeature.xsd
|
||||
http://www.opengis.net/samplingSpatial/2.0 http://schemas.opengis.net/samplingSpatial/2.0/spatialSamplingFeature.xsd
|
||||
http://www.opengis.net/swe/2.0 http://schemas.opengis.net/sweCommon/2.0/swe.xsd
|
||||
http://inspire.ec.europa.eu/schemas/ompr/3.0 http://inspire.ec.europa.eu/schemas/ompr/3.0/Processes.xsd
|
||||
http://inspire.ec.europa.eu/schemas/omso/3.0 http://inspire.ec.europa.eu/schemas/omso/3.0/SpecialisedObservations.xsd
|
||||
http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0 http://xml.fmi.fi/schema/om/atmosphericfeatures/1.0/atmosphericfeatures.xsd">
|
||||
|
||||
<wfs:member>
|
||||
<omso:GridSeriesObservation
|
||||
gml:id="WFS-N4yxAM8P4I023XBe9o.q4oicpFWJTowroWbbpdOt.Lnl5dsPTTv3c3Trvlw9NGXk6dbeuzpp4b9O7pj39svLDnywtLFlz6d1TTty2o_4Uap43jhMbHy51qRaFOO6dNGTVwzsu7JU07ctqP.FGqePJojO15fPffyyVOjXl899_LJf39svLvy09MOLZliZmzD0y8.kTM2b8eHZlrUzab8aSu69MzhrbcPiJp59MO7HlpWroQGltw.IvDfj0c5wY5m9ty9Mu.hh5YduXpl5c6xujLbWJy0Vod8l9iw26d1aHfnfYsNundWh3z32LDbp3VlcL_PLha23Tz56d2epl8dKxp2Gc2t3XbPzU.mHpp37uc4zM4bMOPLzrM4b.Xbfva3Hrh2aenmTuzb4mtz6YemnfuqeeGWtDfwy7smHphbnPpv5ZcnHrl5eb.nJW6Fm26XTrfi55eXbD00793N0675cPTRl5OnW3rs6aeG_Tu6Y9_bLyw58rQ6aduWn0y8J.Qmh007ctrfuy1jVakMA">
|
||||
|
||||
<om:phenomenonTime>
|
||||
<gml:TimePeriod gml:id="time1-1-1">
|
||||
<gml:beginPosition>2019-03-10T10:10:00Z</gml:beginPosition>
|
||||
<gml:endPosition>2019-03-10T20:10:00Z</gml:endPosition>
|
||||
</gml:TimePeriod>
|
||||
</om:phenomenonTime>
|
||||
<om:resultTime>
|
||||
<gml:TimeInstant gml:id="time2-1-1">
|
||||
<gml:timePosition>2019-03-10T20:10:00Z</gml:timePosition>
|
||||
</gml:TimeInstant>
|
||||
</om:resultTime>
|
||||
|
||||
<om:procedure xlink:href="http://xml.fmi.fi/inspire/process/opendata" />
|
||||
<om:parameter>
|
||||
<om:NamedValue>
|
||||
<om:name
|
||||
xlink:href="http://inspire.ec.europa.eu/codeList/ProcessParameterValue/value/groundObservation/observationIntent" />
|
||||
<om:value>
|
||||
atmosphere
|
||||
</om:value>
|
||||
</om:NamedValue>
|
||||
</om:parameter>
|
||||
|
||||
<om:observedProperty
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea&language=eng" />
|
||||
<om:featureOfInterest>
|
||||
<sams:SF_SpatialSamplingFeature gml:id="sampling-feature-1-1-fmisid">
|
||||
|
||||
<sam:sampledFeature>
|
||||
<target:LocationCollection gml:id="sampled-target-1-1">
|
||||
<target:member>
|
||||
<target:Location gml:id="obsloc-fmisid-100683-pos">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/fmisid">100683</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">Porvoo Kilpilahti satama</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">-16777356</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/wmo">2994</gml:name>
|
||||
<target:representativePoint xlink:href="#point-100683" />
|
||||
|
||||
|
||||
<target:region codeSpace="http://xml.fmi.fi/namespace/location/region">Porvoo</target:region>
|
||||
|
||||
</target:Location>
|
||||
</target:member>
|
||||
<target:member>
|
||||
<target:Location gml:id="obsloc-fmisid-101023-pos">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/fmisid">101023</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">Porvoo Emäsalo</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">-16000110</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/wmo">2991</gml:name>
|
||||
<target:representativePoint xlink:href="#point-101023" />
|
||||
|
||||
|
||||
<target:region codeSpace="http://xml.fmi.fi/namespace/location/region">Porvoo</target:region>
|
||||
|
||||
</target:Location>
|
||||
</target:member>
|
||||
<target:member>
|
||||
<target:Location gml:id="obsloc-fmisid-101028-pos">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/fmisid">101028</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">Porvoo Harabacka</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">-16000142</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/wmo">2759</gml:name>
|
||||
<target:representativePoint xlink:href="#point-101028" />
|
||||
|
||||
|
||||
<target:region codeSpace="http://xml.fmi.fi/namespace/location/region">Porvoo</target:region>
|
||||
|
||||
</target:Location>
|
||||
</target:member>
|
||||
</target:LocationCollection>
|
||||
</sam:sampledFeature>
|
||||
<sams:shape>
|
||||
<gml:MultiPoint gml:id="mp-1-1-fmisid">
|
||||
<gml:pointMember>
|
||||
<gml:Point gml:id="point-100683" srsName="http://www.opengis.net/def/crs/EPSG/0/4258" srsDimension="2">
|
||||
<gml:name>Porvoo Kilpilahti satama</gml:name>
|
||||
<gml:pos>60.30373 25.54916 </gml:pos>
|
||||
</gml:Point>
|
||||
</gml:pointMember>
|
||||
<gml:pointMember>
|
||||
<gml:Point gml:id="point-101023" srsName="http://www.opengis.net/def/crs/EPSG/0/4258" srsDimension="2">
|
||||
<gml:name>Porvoo Emäsalo</gml:name>
|
||||
<gml:pos>60.20382 25.62546 </gml:pos>
|
||||
</gml:Point>
|
||||
</gml:pointMember>
|
||||
<gml:pointMember>
|
||||
<gml:Point gml:id="point-101028" srsName="http://www.opengis.net/def/crs/EPSG/0/4258" srsDimension="2">
|
||||
<gml:name>Porvoo Harabacka</gml:name>
|
||||
<gml:pos>60.39172 25.60730 </gml:pos>
|
||||
</gml:Point>
|
||||
</gml:pointMember>
|
||||
</gml:MultiPoint>
|
||||
</sams:shape>
|
||||
</sams:SF_SpatialSamplingFeature>
|
||||
</om:featureOfInterest>
|
||||
|
||||
<om:result>
|
||||
<gmlcov:MultiPointCoverage gml:id="mpcv1-1-1">
|
||||
<gml:domainSet>
|
||||
<gmlcov:SimpleMultiPoint gml:id="mp1-1-1"
|
||||
srsName="http://xml.fmi.fi/gml/crs/compoundCRS.php?crs=4258&time=unixtime" srsDimension="3">
|
||||
<gmlcov:positions>
|
||||
60.30373 25.54916 1552215600
|
||||
60.30373 25.54916 1552219200
|
||||
60.30373 25.54916 1552222800
|
||||
60.30373 25.54916 1552226400
|
||||
60.30373 25.54916 1552230000
|
||||
60.30373 25.54916 1552233600
|
||||
60.30373 25.54916 1552237200
|
||||
60.30373 25.54916 1552240800
|
||||
60.30373 25.54916 1552244400
|
||||
60.30373 25.54916 1552248000
|
||||
60.20382 25.62546 1552215600
|
||||
60.20382 25.62546 1552219200
|
||||
60.20382 25.62546 1552222800
|
||||
60.20382 25.62546 1552226400
|
||||
60.20382 25.62546 1552230000
|
||||
60.20382 25.62546 1552233600
|
||||
60.20382 25.62546 1552237200
|
||||
60.20382 25.62546 1552240800
|
||||
60.20382 25.62546 1552244400
|
||||
60.20382 25.62546 1552248000
|
||||
60.39172 25.60730 1552215600
|
||||
60.39172 25.60730 1552219200
|
||||
60.39172 25.60730 1552222800
|
||||
60.39172 25.60730 1552226400
|
||||
60.39172 25.60730 1552230000
|
||||
60.39172 25.60730 1552233600
|
||||
60.39172 25.60730 1552237200
|
||||
60.39172 25.60730 1552240800
|
||||
60.39172 25.60730 1552244400
|
||||
60.39172 25.60730 1552248000
|
||||
</gmlcov:positions>
|
||||
</gmlcov:SimpleMultiPoint>
|
||||
</gml:domainSet>
|
||||
<gml:rangeSet>
|
||||
<gml:DataBlock>
|
||||
<gml:rangeParameters />
|
||||
<gml:doubleOrNilReasonTupleList>
|
||||
-0.5 73.0 299.0 5.3 8.2 NaN
|
||||
-0.6 65.0 293.0 7.0 9.1 NaN
|
||||
-0.9 60.0 300.0 6.2 9.8 NaN
|
||||
-1.2 59.0 288.0 6.3 8.9 NaN
|
||||
-1.2 57.0 256.0 4.6 7.1 NaN
|
||||
-1.6 64.0 232.0 2.4 5.2 NaN
|
||||
-1.9 66.0 239.0 1.9 3.2 NaN
|
||||
-2.3 65.0 249.0 3.1 5.0 NaN
|
||||
-2.9 71.0 280.0 4.3 5.7 NaN
|
||||
-3.3 77.0 246.0 3.4 5.6 NaN
|
||||
-0.4 77.0 312.0 8.0 10.0 985.9
|
||||
0.0 70.0 286.0 7.5 9.0 986.5
|
||||
0.1 61.0 295.0 8.6 10.5 987.0
|
||||
-1.0 64.0 282.0 8.4 10.5 987.6
|
||||
-1.2 65.0 271.0 6.6 8.7 988.1
|
||||
-1.3 61.0 262.0 5.0 6.7 988.2
|
||||
-1.2 65.0 243.0 8.2 9.6 988.1
|
||||
-1.5 69.0 252.0 6.1 7.6 987.9
|
||||
-1.7 71.0 262.0 7.3 8.6 988.0
|
||||
-2.4 77.0 276.0 6.0 7.5 988.2
|
||||
-0.6 74.0 317.0 3.9 7.1 985.9
|
||||
-0.9 64.0 290.0 4.2 6.8 986.4
|
||||
-1.0 58.0 296.0 5.3 9.1 987.0
|
||||
-1.7 66.0 301.0 3.5 6.6 987.6
|
||||
-1.9 64.0 269.0 2.6 4.2 988.0
|
||||
-2.8 71.0 231.0 1.1 2.3 988.1
|
||||
-3.4 78.0 229.0 1.1 1.6 988.1
|
||||
-3.8 79.0 229.0 1.8 2.7 987.8
|
||||
-4.1 81.0 253.0 2.0 3.2 988.0
|
||||
-4.7 86.0 224.0 1.9 3.1 988.2
|
||||
</gml:doubleOrNilReasonTupleList>
|
||||
</gml:DataBlock>
|
||||
</gml:rangeSet>
|
||||
<gml:coverageFunction>
|
||||
<gml:CoverageMappingRule>
|
||||
<gml:ruleDefinition>Linear</gml:ruleDefinition>
|
||||
</gml:CoverageMappingRule>
|
||||
</gml:coverageFunction>
|
||||
<gmlcov:rangeType>
|
||||
<swe:DataRecord>
|
||||
<swe:field name="t2m"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=t2m&language=eng" />
|
||||
<swe:field name="rh"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=rh&language=eng" />
|
||||
<swe:field name="wd_10min"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=wd_10min&language=eng" />
|
||||
<swe:field name="ws_10min"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=ws_10min&language=eng" />
|
||||
<swe:field name="wg_10min"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=wg_10min&language=eng" />
|
||||
<swe:field name="p_sea"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=p_sea&language=eng" />
|
||||
</swe:DataRecord>
|
||||
</gmlcov:rangeType>
|
||||
</gmlcov:MultiPointCoverage>
|
||||
</om:result>
|
||||
|
||||
</omso:GridSeriesObservation>
|
||||
</wfs:member>
|
||||
</wfs:FeatureCollection>
|
||||
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- https://opendata.fmi.fi/wfs?service=WFS&version=2.0.0&request=getFeature&storedquery_id=fmi::observations::weather::timevaluepair&fmisid=101023&starttime=2019-03-10T10:10:00Z&endtime=2019-03-10T20:10:00Z×tep=60¶meters=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea -->
|
||||
<wfs:FeatureCollection timeStamp="2019-03-22T21:08:05Z" numberMatched="1" numberReturned="1"
|
||||
xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:om="http://www.opengis.net/om/2.0"
|
||||
xmlns:ompr="http://inspire.ec.europa.eu/schemas/ompr/3.0" xmlns:omso="http://inspire.ec.europa.eu/schemas/omso/3.0"
|
||||
xmlns:gml="http://www.opengis.net/gml/3.2" xmlns:gmd="http://www.isotc211.org/2005/gmd"
|
||||
xmlns:gco="http://www.isotc211.org/2005/gco" xmlns:swe="http://www.opengis.net/swe/2.0"
|
||||
xmlns:gmlcov="http://www.opengis.net/gmlcov/1.0" xmlns:sam="http://www.opengis.net/sampling/2.0"
|
||||
xmlns:sams="http://www.opengis.net/samplingSpatial/2.0"
|
||||
xmlns:target="http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0"
|
||||
xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd
|
||||
http://www.opengis.net/gmlcov/1.0 http://schemas.opengis.net/gmlcov/1.0/gmlcovAll.xsd
|
||||
http://www.opengis.net/sampling/2.0 http://schemas.opengis.net/sampling/2.0/samplingFeature.xsd
|
||||
http://www.opengis.net/samplingSpatial/2.0 http://schemas.opengis.net/samplingSpatial/2.0/spatialSamplingFeature.xsd
|
||||
http://www.opengis.net/swe/2.0 http://schemas.opengis.net/sweCommon/2.0/swe.xsd
|
||||
http://inspire.ec.europa.eu/schemas/ompr/3.0 http://inspire.ec.europa.eu/schemas/ompr/3.0/Processes.xsd
|
||||
http://inspire.ec.europa.eu/schemas/omso/3.0 http://inspire.ec.europa.eu/schemas/omso/3.0/SpecialisedObservations.xsd
|
||||
http://xml.fmi.fi/namespace/om/atmosphericfeatures/1.0 http://xml.fmi.fi/schema/om/atmosphericfeatures/1.0/atmosphericfeatures.xsd">
|
||||
|
||||
<wfs:member>
|
||||
<omso:GridSeriesObservation
|
||||
gml:id="WFS-xZ0lqfETUd73TKo3ljaXS8obGT2JTowroWbbpdOt.Lnl5dsPTTv3c3Trvlw9NGXk6dbeuzpp4b9O7pj39svLDnywtLFlz6d1TTty2o_4Uap43jhMbHy51qRaFOO6dNGTVwzsu7JU07ctqP.FGqePJojOzbdPPTk5yf6RRmdry.e._lkqdGvL577.WS_v7ZeXflp6YcWzLEzNmHpl59ImZs348OzLWpm0340ld16ZnDW24fETTz6Yd2PLStXQgNLbh8ReG_Ho5zgxzN7bl6Zd9DDyw7cvTLy51jdGW2sTlorQ75L7Fht07q0O_O.xYbdO6tDvnvsWG3TurK4X.eXC1tunnz07s9TL46VjTsM5tbuu2fmp9MPTTv3c5wmtx64dmnp5k7s2.Jrc.mHpp37qnnhlrQ38Mu7Jh6YW5z6b.WXJx65eXm_pyVuhZtul0634ueXl2w9NO_dzdOu.XD00ZeTp1t67Omnhv07umPf2y8sOfK0Omnblp9MvCfkJodNO3La37stY1WpDAA--">
|
||||
|
||||
<om:phenomenonTime>
|
||||
<gml:TimePeriod gml:id="time1-1-1">
|
||||
<gml:beginPosition>2019-03-10T10:10:00Z</gml:beginPosition>
|
||||
<gml:endPosition>2019-03-10T20:10:00Z</gml:endPosition>
|
||||
</gml:TimePeriod>
|
||||
</om:phenomenonTime>
|
||||
<om:resultTime>
|
||||
<gml:TimeInstant gml:id="time2-1-1">
|
||||
<gml:timePosition>2019-03-10T20:10:00Z</gml:timePosition>
|
||||
</gml:TimeInstant>
|
||||
</om:resultTime>
|
||||
|
||||
<om:procedure xlink:href="http://xml.fmi.fi/inspire/process/opendata" />
|
||||
<om:parameter>
|
||||
<om:NamedValue>
|
||||
<om:name
|
||||
xlink:href="http://inspire.ec.europa.eu/codeList/ProcessParameterValue/value/groundObservation/observationIntent" />
|
||||
<om:value>
|
||||
atmosphere
|
||||
</om:value>
|
||||
</om:NamedValue>
|
||||
</om:parameter>
|
||||
|
||||
<om:observedProperty
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea&language=eng" />
|
||||
<om:featureOfInterest>
|
||||
<sams:SF_SpatialSamplingFeature gml:id="sampling-feature-1-1-fmisid">
|
||||
|
||||
<sam:sampledFeature>
|
||||
<target:LocationCollection gml:id="sampled-target-1-1">
|
||||
<target:member>
|
||||
<target:Location gml:id="obsloc-fmisid-101023-pos">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/fmisid">101023</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">Porvoo Emäsalo</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">-16000110</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/wmo">2991</gml:name>
|
||||
<target:representativePoint xlink:href="#point-101023" />
|
||||
|
||||
|
||||
<target:region codeSpace="http://xml.fmi.fi/namespace/location/region">Porvoo</target:region>
|
||||
|
||||
</target:Location>
|
||||
</target:member>
|
||||
</target:LocationCollection>
|
||||
</sam:sampledFeature>
|
||||
<sams:shape>
|
||||
<gml:MultiPoint gml:id="mp-1-1-fmisid">
|
||||
<gml:pointMember>
|
||||
<gml:Point gml:id="point-101023" srsName="http://www.opengis.net/def/crs/EPSG/0/4258" srsDimension="2">
|
||||
<gml:name>Porvoo Emäsalo</gml:name>
|
||||
<gml:pos>60.20382 25.62546 </gml:pos>
|
||||
</gml:Point>
|
||||
</gml:pointMember>
|
||||
</gml:MultiPoint>
|
||||
</sams:shape>
|
||||
</sams:SF_SpatialSamplingFeature>
|
||||
</om:featureOfInterest>
|
||||
|
||||
<om:result>
|
||||
<gmlcov:MultiPointCoverage gml:id="mpcv1-1-1">
|
||||
<gml:domainSet>
|
||||
<gmlcov:SimpleMultiPoint gml:id="mp1-1-1"
|
||||
srsName="http://xml.fmi.fi/gml/crs/compoundCRS.php?crs=4258&time=unixtime" srsDimension="3">
|
||||
<gmlcov:positions>
|
||||
60.20382 25.62546 1552215600
|
||||
60.20382 25.62546 1552219200
|
||||
60.20382 25.62546 1552222800
|
||||
60.20382 25.62546 1552226400
|
||||
60.20382 25.62546 1552230000
|
||||
60.20382 25.62546 1552233600
|
||||
60.20382 25.62546 1552237200
|
||||
60.20382 25.62546 1552240800
|
||||
60.20382 25.62546 1552244400
|
||||
60.20382 25.62546 1552248000
|
||||
</gmlcov:positions>
|
||||
</gmlcov:SimpleMultiPoint>
|
||||
</gml:domainSet>
|
||||
<gml:rangeSet>
|
||||
<gml:DataBlock>
|
||||
<gml:rangeParameters />
|
||||
<gml:doubleOrNilReasonTupleList>
|
||||
-0.4 77.0 312.0 8.0 10.0 985.9
|
||||
0.0 70.0 286.0 7.5 9.0 986.5
|
||||
0.1 61.0 295.0 8.6 10.5 987.0
|
||||
-1.0 64.0 282.0 8.4 10.5 987.6
|
||||
-1.2 65.0 271.0 6.6 8.7 988.1
|
||||
-1.3 61.0 262.0 5.0 6.7 988.2
|
||||
-1.2 65.0 243.0 8.2 9.6 988.1
|
||||
-1.5 69.0 252.0 6.1 7.6 987.9
|
||||
-1.7 71.0 262.0 7.3 8.6 988.0
|
||||
-2.4 77.0 276.0 6.0 7.5 988.2
|
||||
</gml:doubleOrNilReasonTupleList>
|
||||
</gml:DataBlock>
|
||||
</gml:rangeSet>
|
||||
<gml:coverageFunction>
|
||||
<gml:CoverageMappingRule>
|
||||
<gml:ruleDefinition>Linear</gml:ruleDefinition>
|
||||
</gml:CoverageMappingRule>
|
||||
</gml:coverageFunction>
|
||||
<gmlcov:rangeType>
|
||||
<swe:DataRecord>
|
||||
<swe:field name="t2m"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=t2m&language=eng" />
|
||||
<swe:field name="rh"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=rh&language=eng" />
|
||||
<swe:field name="wd_10min"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=wd_10min&language=eng" />
|
||||
<swe:field name="ws_10min"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=ws_10min&language=eng" />
|
||||
<swe:field name="wg_10min"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=wg_10min&language=eng" />
|
||||
<swe:field name="p_sea"
|
||||
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&param=p_sea&language=eng" />
|
||||
</swe:DataRecord>
|
||||
</gmlcov:rangeType>
|
||||
</gmlcov:MultiPointCoverage>
|
||||
</om:result>
|
||||
|
||||
</omso:GridSeriesObservation>
|
||||
</wfs:member>
|
||||
</wfs:FeatureCollection>
|
||||
@@ -0,0 +1,140 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- http://opendata.fmi.fi/wfs/fin?service=WFS&version=2.0.0&request=GetFeature&storedquery_id=fmi::ef::stations&networkid=121& -->
|
||||
<wfs:FeatureCollection timeStamp="2019-03-24T17:18:25Z" numberMatched="186" numberReturned="186"
|
||||
xmlns:wfs="http://www.opengis.net/wfs/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:gml="http://www.opengis.net/gml/3.2"
|
||||
xmlns:gco="http://www.isotc211.org/2005/gco" xmlns:ins_base="http://inspire.ec.europa.eu/schemas/base/3.3"
|
||||
xmlns:ins_base2="http://inspire.ec.europa.eu/schemas/base2/2.0" xmlns:ef="http://inspire.ec.europa.eu/schemas/ef/4.0"
|
||||
xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0/wfs.xsd
|
||||
http://inspire.ec.europa.eu/schemas/ef/4.0 http://inspire.ec.europa.eu/schemas/ef/4.0/EnvironmentalMonitoringFacilities.xsd">
|
||||
|
||||
<wfs:member>
|
||||
<ef:EnvironmentalMonitoringFacility
|
||||
gml:id="WFS-vrkQ87m4.0LG38lzogcLEyonCriJTowsWbbpdOsuZ0659MPTTv3c4W9h69NG_lp6eYm_bh07q4tHTpwdL1_jbsXZtuldm0tLFlz6d1TTty2o_4Ubdw47hM7Hsw8.cnJJjEZ2XdkqaduW1H_CjcOHHcJwad3Php5ZZ2Hbl58MOPLXaFo6dODpev8bdi7Nt0rs2lfuw7cvPhhx5V.nJl3dNObTl5L.fTD0079y_Tu58NPLK7uejf3n4ueXl207s8PDww4tOzT08xNLn0w9NO_dJyVmMWDBs4ZtLn038sOfLJySXXG5z6b.WXJx65eXm_pyVxZtul06y5nTrn0w9NO_dzA-">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/fmisid">100683</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">Porvoo Kilpilahti satama</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">-16777356</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/wmo">02994</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/location/region">Porvoo</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/location/country">Suomi</gml:name>
|
||||
<ef:inspireId>
|
||||
<ins_base:Identifier>
|
||||
<ins_base:localId>100683</ins_base:localId>
|
||||
<ins_base:namespace>http://xml.fmi.fi/namespace/identifier/station/inspire</ins_base:namespace>
|
||||
</ins_base:Identifier>
|
||||
</ef:inspireId>
|
||||
<ef:name>Porvoo Kilpilahti satama</ef:name>
|
||||
<ef:mediaMonitored xlink:href="" nilReason="missing" />
|
||||
<ef:representativePoint>
|
||||
<gml:Point gml:id="point-1" axisLabels="Lat Long" srsName="http://www.opengis.net/def/crs/EPSG/0/4258"
|
||||
srsDimension="2">
|
||||
<gml:pos>60.303725 25.549164</gml:pos>
|
||||
</gml:Point>
|
||||
</ef:representativePoint>
|
||||
<ef:measurementRegime
|
||||
xlink:href="http://inspire.ec.europa.eu/codelist/MeasurementRegimeValue/continuousDataCollection" />
|
||||
<ef:mobile>false</ef:mobile>
|
||||
<ef:operationalActivityPeriod>
|
||||
<ef:OperationalActivityPeriod gml:id="oap-1-1">
|
||||
<ef:activityTime>
|
||||
<gml:TimePeriod gml:id="oap-tp-1-1">
|
||||
<gml:beginPosition>2014-06-19T00:00:00Z</gml:beginPosition>
|
||||
<gml:endPosition indeterminatePosition="now" />
|
||||
</gml:TimePeriod>
|
||||
</ef:activityTime>
|
||||
</ef:OperationalActivityPeriod>
|
||||
</ef:operationalActivityPeriod>
|
||||
<ef:belongsTo xlink:title="Automaattinen sääasema"
|
||||
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&storedquery_id=fmi::ef::networks&networkid=121&" />
|
||||
</ef:EnvironmentalMonitoringFacility>
|
||||
</wfs:member>
|
||||
<wfs:member>
|
||||
<ef:EnvironmentalMonitoringFacility
|
||||
gml:id="WFS-II3Xuz6KyPwy8qiVxHNgeRWBtLWJTowsWbbpdOsuZ0659MPTTv3c4W9h69NG_lp6eYm_bh07q4tHTpwdL1_jbsXZtuldm0tLFlz6d1TTty2o_4Ubdw47hM7Hsw8.cnJJjEZ2XdkqaduW1H_CjcOHHcJwad3Php5ZZ2Hbl58MOPLXaFo6dODpev8bdi7Nt0rs2lfuw7cvPhhx5V.nJl3dNObTl5L.fTD0079y_Tu58NPLK7uejf3n4ueXl207s8PDww4tOzT08xNLn0w9NO_dJyVmMWDBywcNLn038sOfLJySXXG5z6b.WXJx65eXm_pyVxZtul06y5nTrn0w9NO_dzA-">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/fmisid">100908</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">Parainen Utö</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">-16000054</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/wmo">02981</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/location/region">Parainen</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/location/country">Suomi</gml:name>
|
||||
<ef:inspireId>
|
||||
<ins_base:Identifier>
|
||||
<ins_base:localId>100908</ins_base:localId>
|
||||
<ins_base:namespace>http://xml.fmi.fi/namespace/identifier/station/inspire</ins_base:namespace>
|
||||
</ins_base:Identifier>
|
||||
</ef:inspireId>
|
||||
<ef:name>Parainen Utö</ef:name>
|
||||
<ef:mediaMonitored xlink:href="" nilReason="missing" />
|
||||
<ef:representativePoint>
|
||||
<gml:Point gml:id="point-2" axisLabels="Lat Long" srsName="http://www.opengis.net/def/crs/EPSG/0/4258"
|
||||
srsDimension="2">
|
||||
<gml:pos>59.779094 21.374788</gml:pos>
|
||||
</gml:Point>
|
||||
</ef:representativePoint>
|
||||
<ef:measurementRegime
|
||||
xlink:href="http://inspire.ec.europa.eu/codelist/MeasurementRegimeValue/continuousDataCollection" />
|
||||
<ef:mobile>false</ef:mobile>
|
||||
<ef:operationalActivityPeriod>
|
||||
<ef:OperationalActivityPeriod gml:id="oap-2-1">
|
||||
<ef:activityTime>
|
||||
<gml:TimePeriod gml:id="oap-tp-2-1">
|
||||
<gml:beginPosition>1881-02-01T00:00:00Z</gml:beginPosition>
|
||||
<gml:endPosition indeterminatePosition="now" />
|
||||
</gml:TimePeriod>
|
||||
</ef:activityTime>
|
||||
</ef:OperationalActivityPeriod>
|
||||
</ef:operationalActivityPeriod>
|
||||
<ef:belongsTo xlink:title="Automaattinen sääasema"
|
||||
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&storedquery_id=fmi::ef::networks&networkid=121&" />
|
||||
<ef:belongsTo xlink:title="Sadeasema"
|
||||
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&storedquery_id=fmi::ef::networks&networkid=124&" />
|
||||
<ef:belongsTo xlink:title="Auringonsäteilyasema"
|
||||
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&storedquery_id=fmi::ef::networks&networkid=128&" />
|
||||
<ef:belongsTo xlink:title="Ilmanlaadun tausta-asema"
|
||||
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&storedquery_id=fmi::ef::networks&networkid=129&" />
|
||||
<ef:belongsTo xlink:title="Tutkimusmittausasema"
|
||||
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&storedquery_id=fmi::ef::networks&networkid=146&" />
|
||||
</ef:EnvironmentalMonitoringFacility>
|
||||
</wfs:member>
|
||||
<wfs:member>
|
||||
<ef:EnvironmentalMonitoringFacility
|
||||
gml:id="WFS-gEH2bGdgHgyFDHlZfVTKz_hRejSJTowsWbbpdOsuZ0659MPTTv3c4W9h69NG_lp6eYm_bh07q4tHTpwdL1_jbsXZtuldm0tLFlz6d1TTty2o_4Ubdw47hM7Hsw8.cnJJjEZ2XdkqaduW1H_CjcOHHcJwad3Php5ZZ2Hbl58MOPLXaFo6dODpev8bdi7Nt0rs2lfuw7cvPhhx5V.nJl3dNObTl5L.fTD0079y_Tu58NPLK7uejf3n4ueXl207s8PDww4tOzT08xNLn0w9NO_dJyVmMWDBywctLn038sOfLJySXXG5z6b.WXJx65eXm_pyVxZtul06y5nTrn0w9NO_dzA-">
|
||||
<gml:identifier codeSpace="http://xml.fmi.fi/namespace/stationcode/fmisid">100909</gml:identifier>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/name">Lemland Nyhamn</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/geoid">-16000086</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/locationcode/wmo">02980</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/location/region">Lemland</gml:name>
|
||||
<gml:name codeSpace="http://xml.fmi.fi/namespace/location/country">Suomi</gml:name>
|
||||
<ef:inspireId>
|
||||
<ins_base:Identifier>
|
||||
<ins_base:localId>100909</ins_base:localId>
|
||||
<ins_base:namespace>http://xml.fmi.fi/namespace/identifier/station/inspire</ins_base:namespace>
|
||||
</ins_base:Identifier>
|
||||
</ef:inspireId>
|
||||
<ef:name>Lemland Nyhamn</ef:name>
|
||||
<ef:mediaMonitored xlink:href="" nilReason="missing" />
|
||||
<ef:representativePoint>
|
||||
<gml:Point gml:id="point-3" axisLabels="Lat Long" srsName="http://www.opengis.net/def/crs/EPSG/0/4258"
|
||||
srsDimension="2">
|
||||
<gml:pos>59.959108 19.953736</gml:pos>
|
||||
</gml:Point>
|
||||
</ef:representativePoint>
|
||||
<ef:measurementRegime
|
||||
xlink:href="http://inspire.ec.europa.eu/codelist/MeasurementRegimeValue/continuousDataCollection" />
|
||||
<ef:mobile>false</ef:mobile>
|
||||
<ef:operationalActivityPeriod>
|
||||
<ef:OperationalActivityPeriod gml:id="oap-3-1">
|
||||
<ef:activityTime>
|
||||
<gml:TimePeriod gml:id="oap-tp-3-1">
|
||||
<gml:beginPosition>1958-10-01T00:00:00Z</gml:beginPosition>
|
||||
<gml:endPosition indeterminatePosition="now" />
|
||||
</gml:TimePeriod>
|
||||
</ef:activityTime>
|
||||
</ef:OperationalActivityPeriod>
|
||||
</ef:operationalActivityPeriod>
|
||||
<ef:belongsTo xlink:title="Automaattinen sääasema"
|
||||
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&storedquery_id=fmi::ef::networks&networkid=121&" />
|
||||
</ef:EnvironmentalMonitoringFacility>
|
||||
</wfs:member>
|
||||
|
||||
</wfs:FeatureCollection>
|
||||
Reference in New Issue
Block a user