added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View 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>

View 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>

View 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

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

View 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>

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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";
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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]));
}
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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("&timestep=").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);
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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")));
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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 2026 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>

View File

@@ -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();
}
}
}

View File

@@ -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&timestep=61&fmisid=101023"
+ "&parameters=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&timestep=61&latlon=9,8"
+ "&parameters=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&timestep=61&lat=MYLAT&lon=FOO&special=x,y,z"
+ "&parameters=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea,r_1h,snow_aws,vis,n_man,wawa"));
}
}

View File

@@ -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));
}
}

View File

@@ -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&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]"));
return;
} catch (Throwable e) {
fail("Wrong exception, was " + e.getClass().getName());
}
fail("FMIResponseException expected");
}
}

View File

@@ -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>"));
}
}

View File

@@ -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)));
}
}

View File

@@ -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)));
}
}

View File

@@ -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")))));
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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&timestep=60&parameters=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&amp;fmisid=101023&amp;parameters=t2m%2Crh%2Cwd_10min%2Cws_10min%2Cwg_10min%2Cp_sea&amp;request=getFeature&amp;service=WFS&amp;starttime=2019-03-10T10%3A10%3A00Z&amp;storedquery_id=fmi%3A%3Aobservations%3A%3Aweather%3A%3Amultipointcoverage&amp;timestep=60&amp;version=2.0.0</ExceptionText>
</Exception>
</ExceptionReport>

View File

@@ -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&timestep=360&parameters=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&amp;param=Temperature,Humidity&amp;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&amp;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&amp;param=Temperature&amp;language=eng" />
<swe:field name="Humidity"
xlink:href="http://opendata.fmi.fi/meta?observableProperty=forecast&amp;param=Humidity&amp;language=eng" />
</swe:DataRecord>
</gmlcov:rangeType>
</gmlcov:MultiPointCoverage>
</om:result>
</omso:GridSeriesObservation>
</wfs:member>
</wfs:FeatureCollection>

View File

@@ -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&timestep=60&parameters=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>

View File

@@ -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&timestep=60&parameters=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&amp;param=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea&amp;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&amp;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&amp;param=t2m&amp;language=eng" />
<swe:field name="rh"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=rh&amp;language=eng" />
<swe:field name="wd_10min"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=wd_10min&amp;language=eng" />
<swe:field name="ws_10min"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=ws_10min&amp;language=eng" />
<swe:field name="wg_10min"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=wg_10min&amp;language=eng" />
<swe:field name="p_sea"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=p_sea&amp;language=eng" />
</swe:DataRecord>
</gmlcov:rangeType>
</gmlcov:MultiPointCoverage>
</om:result>
</omso:GridSeriesObservation>
</wfs:member>
</wfs:FeatureCollection>

View File

@@ -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&timestep=60&parameters=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&amp;param=t2m,rh,wd_10min,ws_10min,wg_10min,p_sea&amp;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&amp;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&amp;param=t2m&amp;language=eng" />
<swe:field name="rh"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=rh&amp;language=eng" />
<swe:field name="wd_10min"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=wd_10min&amp;language=eng" />
<swe:field name="ws_10min"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=ws_10min&amp;language=eng" />
<swe:field name="wg_10min"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=wg_10min&amp;language=eng" />
<swe:field name="p_sea"
xlink:href="https://opendata.fmi.fi/meta?observableProperty=observation&amp;param=p_sea&amp;language=eng" />
</swe:DataRecord>
</gmlcov:rangeType>
</gmlcov:MultiPointCoverage>
</om:result>
</omso:GridSeriesObservation>
</wfs:member>
</wfs:FeatureCollection>

View File

@@ -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&amp;storedquery_id=fmi::ef::networks&amp;networkid=121&amp;" />
</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&amp;storedquery_id=fmi::ef::networks&amp;networkid=121&amp;" />
<ef:belongsTo xlink:title="Sadeasema"
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&amp;storedquery_id=fmi::ef::networks&amp;networkid=124&amp;" />
<ef:belongsTo xlink:title="Auringonsäteilyasema"
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&amp;storedquery_id=fmi::ef::networks&amp;networkid=128&amp;" />
<ef:belongsTo xlink:title="Ilmanlaadun tausta-asema"
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&amp;storedquery_id=fmi::ef::networks&amp;networkid=129&amp;" />
<ef:belongsTo xlink:title="Tutkimusmittausasema"
xlink:href="http://opendata.fmi.fi/wfs/fin?request=getFeature&amp;storedquery_id=fmi::ef::networks&amp;networkid=146&amp;" />
</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&amp;storedquery_id=fmi::ef::networks&amp;networkid=121&amp;" />
</ef:EnvironmentalMonitoringFacility>
</wfs:member>
</wfs:FeatureCollection>