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,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.smhi-${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-smhi" description="SMHI Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.smhi/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,54 @@
/**
* 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.smhi.internal;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A class containing a forecast for a specific point in time.
*
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class Forecast implements Comparable<Forecast> {
private final ZonedDateTime validTime;
private final Map<String, @Nullable BigDecimal> parameters;
public Forecast(ZonedDateTime validTime, Map<String, @Nullable BigDecimal> parameters) {
this.validTime = validTime;
this.parameters = parameters;
}
public ZonedDateTime getValidTime() {
return validTime;
}
public Map<String, @Nullable BigDecimal> getParameters() {
return parameters;
}
public @Nullable BigDecimal getParameter(String parameter) {
return parameters.get(parameter);
}
@Override
public int compareTo(Forecast o) {
return validTime.compareTo(o.validTime);
}
}

View File

@@ -0,0 +1,97 @@
/**
* 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.smhi.internal;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
/**
* Class with static methods for parsing json strings returned from Smhi
*
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class Parser {
private static JsonParser parser = new JsonParser();
/**
* Parse a json string received from Smhi containing forecasts.
*
* @param json A json string
* @return A {@link TimeSeries} object
*/
public static TimeSeries parseTimeSeries(String json) {
ZonedDateTime referenceTime;
JsonObject object = parser.parse(json).getAsJsonObject();
referenceTime = parseApprovedTime(json);
JsonArray timeSeries = object.get("timeSeries").getAsJsonArray();
List<Forecast> forecasts = StreamSupport.stream(timeSeries.spliterator(), false)
.map(element -> parseForecast(element.getAsJsonObject())).sorted(Comparator.naturalOrder())
.collect(Collectors.toList());
return new TimeSeries(referenceTime, forecasts);
}
/**
* Parse a json string containing the approved time and reference time of the latest forecast
*
* @param json A json string
* @return {@link ZonedDateTime} of the reference time
*/
public static ZonedDateTime parseApprovedTime(String json) {
JsonObject timeObj = parser.parse(json).getAsJsonObject();
return ZonedDateTime.parse(timeObj.get("referenceTime").getAsString());
}
/**
* Parse a single forecast, i.e. a forecast for a specific time.
*
* @param object
* @return
*/
private static Forecast parseForecast(JsonObject object) {
ZonedDateTime validTime = ZonedDateTime.parse(object.get("validTime").getAsString());
Map<String, @Nullable BigDecimal> parameters = new HashMap<>();
JsonArray parameterArray = object.get("parameters").getAsJsonArray();
parameterArray.forEach(element -> {
JsonObject parameterObj = element.getAsJsonObject();
String name = parameterObj.get("name").getAsString().toLowerCase(Locale.ROOT);
BigDecimal value = parameterObj.get("values").getAsJsonArray().get(0).getAsBigDecimal();
parameters.put(name, value);
});
return new Forecast(validTime, parameters);
}
}

View File

@@ -0,0 +1,25 @@
/**
* 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.smhi.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class PointOutOfBoundsException extends Exception {
private static final long serialVersionUID = 546566512L;
}

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.smhi.internal;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SmhiBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class SmhiBindingConstants {
public static final String BINDING_ID = "smhi";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_FORECAST = new ThingTypeUID(BINDING_ID, "forecast");
// Smhi's ids for parameters, also used as channel ids
public static final String PRESSURE = "msl";
public static final String TEMPERATURE = "t";
public static final String VISIBILITY = "vis";
public static final String WIND_DIRECTION = "wd";
public static final String WIND_SPEED = "ws";
public static final String RELATIVE_HUMIDITY = "r";
public static final String THUNDER_PROBABILITY = "tstm";
public static final String TOTAL_CLOUD_COVER = "tcc_mean";
public static final String LOW_CLOUD_COVER = "lcc_mean";
public static final String MEDIUM_CLOUD_COVER = "mcc_mean";
public static final String HIGH_CLOUD_COVER = "hcc_mean";
public static final String GUST = "gust";
public static final String PRECIPITATION_MIN = "pmin";
public static final String PRECIPITATION_MAX = "pmax";
public static final String PRECIPITATION_MEAN = "pmean";
public static final String PRECIPITATION_MEDIAN = "pmedian";
public static final String PERCENT_FROZEN = "spp";
public static final String PRECIPITATION_CATEGORY = "pcat";
public static final String WEATHER_SYMBOL = "wsymb2";
public static final List<String> CHANNEL_IDS = Collections
.unmodifiableList(Stream
.of(PRESSURE, TEMPERATURE, VISIBILITY, WIND_DIRECTION, WIND_SPEED, RELATIVE_HUMIDITY,
THUNDER_PROBABILITY, TOTAL_CLOUD_COVER, LOW_CLOUD_COVER, MEDIUM_CLOUD_COVER,
HIGH_CLOUD_COVER, GUST, PRECIPITATION_MIN, PRECIPITATION_MAX, PRECIPITATION_MEAN,
PRECIPITATION_MEDIAN, PERCENT_FROZEN, PRECIPITATION_CATEGORY, WEATHER_SYMBOL)
.collect(Collectors.toList()));
public static final String BASE_URL = "https://opendata-download-metfcst.smhi.se/api/category/pmp3g/version/2/";
public static final String APPROVED_TIME_URL = BASE_URL + "approvedtime.json";
public static final String POINT_FORECAST_URL = BASE_URL + "geotype/point/lon/%.6f/lat/%.6f/data.json";
public static final BigDecimal OCTAS_TO_PERCENT = BigDecimal.valueOf(12.5);
}

View File

@@ -0,0 +1,31 @@
/**
* 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.smhi.internal;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link SmhiConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class SmhiConfiguration {
public double latitude;
public double longitude;
public @Nullable List<Integer> hourlyForecasts;
public @Nullable List<Integer> dailyForecasts;
}

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.smhi.internal;
import static org.openhab.binding.smhi.internal.SmhiBindingConstants.*;
import java.time.ZonedDateTime;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Class for handling http requests to Smhi's API and return values.
*
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class SmhiConnector {
private final Logger logger = LoggerFactory.getLogger(SmhiConnector.class);
private static final String ACCEPT = "application/json";
private final HttpClient httpClient;
public SmhiConnector(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Get the reference time (the time when the forecast starts) of the latest published forecast
*
* @return A {@link ZonedDateTime} with the time of the latest forecast.
*/
public ZonedDateTime getReferenceTime() throws SmhiException {
logger.debug("Fetching reference time");
Request req = httpClient.newRequest(APPROVED_TIME_URL);
req.accept(ACCEPT);
ContentResponse resp;
try {
resp = req.send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new SmhiException(e);
}
logger.debug("Received response with status {} - {}", resp.getStatus(), resp.getReason());
if (resp.getStatus() == 200) {
return Parser.parseApprovedTime(resp.getContentAsString());
} else {
throw new SmhiException(resp.getReason());
}
}
/**
* Get a forecast for the specified WGS84 coordinates.
*
* @param lat Latitude
* @param lon Longitude
* @return A {@link TimeSeries} object containing the published forecasts.
*/
public TimeSeries getForecast(double lat, double lon) throws SmhiException, PointOutOfBoundsException {
logger.debug("Fetching new forecast");
String url = String.format(Locale.ROOT, POINT_FORECAST_URL, lon, lat);
Request req = httpClient.newRequest(url);
req.accept(ACCEPT);
ContentResponse resp;
try {
resp = req.send();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
throw new SmhiException(e);
}
logger.debug("Received response with status {} - {}", resp.getStatus(), resp.getReason());
switch (resp.getStatus()) {
case 200:
return Parser.parseTimeSeries(resp.getContentAsString());
case 400:
case 404:
throw new PointOutOfBoundsException();
default:
throw new SmhiException(resp.getReason());
}
}
}

View File

@@ -0,0 +1,33 @@
/**
* 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.smhi.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class SmhiException extends Exception {
private static final long serialVersionUID = 516565331L;
public SmhiException(String message) {
super(message);
}
public SmhiException(Throwable cause) {
super(cause);
}
}

View File

@@ -0,0 +1,443 @@
/**
* 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.smhi.internal;
import static org.openhab.binding.smhi.internal.SmhiBindingConstants.*;
import java.math.BigDecimal;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.MetricPrefix;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
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.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SmhiHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class SmhiHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(SmhiHandler.class);
private SmhiConfiguration config = new SmhiConfiguration();
private final HttpClient httpClient;
private @Nullable SmhiConnector connection;
private ZonedDateTime currentHour;
private ZonedDateTime currentDay;
private @Nullable TimeSeries cachedTimeSeries;
private boolean hasLatestForecast = false;
private @Nullable Future<?> forecastUpdater;
private @Nullable Future<?> instantUpdate;
public SmhiHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
this.currentHour = calculateCurrentHour();
this.currentDay = calculateCurrentDay();
}
/**
* Handles commands sent to channels. Since all values are read-only, only REFRESH commands are allowed.
* Sending REFRESH to any item updates all items, since all values are returned in the response from Smhi.
* Therefore there's a wait of 5 seconds before the values are fetched, in which time all other commands are
* blocked, to prevent spamming Smhi's API.
*
* @param channelUID
* @param command
*/
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
updateNow();
}
}
@Override
public void initialize() {
config = getConfigAs(SmhiConfiguration.class);
connection = new SmhiConnector(httpClient);
// Check which channel groups are selected in the config.
List<Channel> channels = new ArrayList<>();
channels.addAll(createChannels());
updateThing(editThing().withChannels(channels).build());
startPolling();
updateNow();
}
/**
* Start polling for updated weather forecast.
*/
private synchronized void startPolling() {
logger.debug("Start polling");
forecastUpdater = scheduler.scheduleWithFixedDelay(this::waitForForecast, 1, 1, TimeUnit.MINUTES);
}
/**
* Cancels all jobs.
*/
private synchronized void cancelPolling() {
logger.debug("Cancelling polling");
Future<?> localRef = forecastUpdater;
if (localRef != null) {
localRef.cancel(false);
}
localRef = instantUpdate;
if (localRef != null) {
localRef.cancel(false);
}
}
/**
* Update channels with new forecast data.
*
* @param timeSeries A {@link TimeSeries} object containing forecasts.
*/
private void updateChannels(TimeSeries timeSeries) {
// Loop through hourly forecasts and update those available
for (int i = 0; i < 25; i++) {
List<Channel> channels = thing.getChannelsOfGroup("hour_" + i);
if (channels.isEmpty()) {
continue;
}
Forecast forecast = timeSeries.getForecast(i);
if (forecast != null) {
channels.forEach(c -> {
String id = c.getUID().getIdWithoutGroup();
BigDecimal value = forecast.getParameter(id);
updateChannel(c, value);
});
}
}
// Loop through daily forecasts and updates those available
for (int i = 0; i < 10; i++) {
List<Channel> channels = thing.getChannelsOfGroup("day_" + i);
if (channels.isEmpty()) {
continue;
}
int offset = 24 * i + 12;
Forecast forecast = timeSeries.getForecast(currentDay, offset);
if (forecast == null) {
if (logger.isDebugEnabled()) {
logger.debug("No forecast yet for {}", currentDay.plusHours(offset));
}
channels.forEach(c -> {
updateState(c.getUID(), UnDefType.NULL);
});
} else {
channels.forEach(c -> {
String id = c.getUID().getIdWithoutGroup();
BigDecimal value = forecast.getParameter(id);
updateChannel(c, value);
});
}
}
}
private void updateChannel(Channel channel, @Nullable BigDecimal value) {
String id = channel.getUID().getIdWithoutGroup();
State newState = UnDefType.NULL;
if (value != null) {
switch (id) {
case PRESSURE:
newState = new QuantityType<>(value, MetricPrefix.HECTO(SIUnits.PASCAL));
break;
case TEMPERATURE:
newState = new QuantityType<>(value, SIUnits.CELSIUS);
break;
case VISIBILITY:
newState = new QuantityType<>(value, MetricPrefix.KILO(SIUnits.METRE));
break;
case WIND_DIRECTION:
newState = new QuantityType<>(value, SmartHomeUnits.DEGREE_ANGLE);
break;
case WIND_SPEED:
case GUST:
newState = new QuantityType<>(value, SmartHomeUnits.METRE_PER_SECOND);
break;
case RELATIVE_HUMIDITY:
case THUNDER_PROBABILITY:
newState = new QuantityType<>(value, SmartHomeUnits.PERCENT);
break;
case PERCENT_FROZEN:
// Smhi returns -9 for spp if there's no precipitation, convert to UNDEF
if (value.intValue() == -9) {
newState = UnDefType.UNDEF;
} else {
newState = new QuantityType<>(value, SmartHomeUnits.PERCENT);
}
break;
case HIGH_CLOUD_COVER:
case MEDIUM_CLOUD_COVER:
case LOW_CLOUD_COVER:
case TOTAL_CLOUD_COVER:
newState = new QuantityType<>(value.multiply(OCTAS_TO_PERCENT), SmartHomeUnits.PERCENT);
break;
case PRECIPITATION_MAX:
case PRECIPITATION_MEAN:
case PRECIPITATION_MEDIAN:
case PRECIPITATION_MIN:
newState = new QuantityType<>(value, SmartHomeUnits.MILLIMETRE_PER_HOUR);
break;
default:
newState = new DecimalType(value);
}
}
updateState(channel.getUID(), newState);
}
/**
* Dispose the {@link org.openhab.core.thing.binding.ThingHandler}. Cancel scheduled jobs
*/
public void dispose() {
cancelPolling();
}
/**
* First check if the time has shifted to a new hour, then start checking if a new forecast have been
* published, in that case, fetch it and update channels.
*/
private void waitForForecast() {
if (isItNewHour()) {
currentHour = calculateCurrentHour();
currentDay = calculateCurrentDay();
// Update channels with cached forecasts - just shift an hour forward
TimeSeries forecast = cachedTimeSeries;
if (forecast != null) {
updateChannels(forecast);
}
hasLatestForecast = false;
}
if (!hasLatestForecast && isForecastUpdated()) {
getUpdatedForecast();
}
}
/**
* Schedules an imminent update, making it wait 5 seconds to catch any bursts of calls before executing.
*/
private synchronized void updateNow() {
Future<?> localRef = instantUpdate;
if (localRef == null || localRef.isDone()) {
instantUpdate = scheduler.schedule(this::getUpdatedForecast, 5, TimeUnit.SECONDS);
} else {
logger.debug("Already waiting for scheduled refresh");
}
}
/**
* Checks if it is a new hour.
*
* @return true if the current time is more than one hour after currentHour, otherwise false.
*/
private boolean isItNewHour() {
return ZonedDateTime.now().minusHours(1).isAfter(currentHour);
}
/**
* Call Smhi's endpoint to check for the time of the last forecast, to see if a new one is available.
*
* @return true if the time of the latest forecast is equal to or after currentHour, otherwise false
*/
private boolean isForecastUpdated() {
ZonedDateTime referenceTime;
SmhiConnector apiConnection = connection;
if (apiConnection != null) {
try {
referenceTime = apiConnection.getReferenceTime();
} catch (SmhiException e) {
return false;
}
return referenceTime.isEqual(currentHour) || referenceTime.isAfter(currentHour);
}
return false;
}
/**
* Fetches latest forecast from Smhi, update channels and check if it was published in the current hour.
* If it is, set flag to indicate we have the latest forecast.
*/
private void getUpdatedForecast() {
TimeSeries forecast;
ZonedDateTime referenceTime;
SmhiConnector apiConnection = connection;
if (apiConnection != null) {
try {
forecast = apiConnection.getForecast(config.latitude, config.longitude);
} catch (SmhiException e) {
String message = e.getCause() == null ? e.getMessage() : e.getCause().getMessage();
logger.debug("Failed to get new forecast: {}", message);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message);
return;
} catch (PointOutOfBoundsException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Coordinates outside valid area");
cancelPolling();
return;
}
updateStatus(ThingStatus.ONLINE);
referenceTime = forecast.getReferenceTime();
updateChannels(forecast);
if (referenceTime.isEqual(currentHour) || referenceTime.isAfter(currentHour)) {
hasLatestForecast = true;
}
cachedTimeSeries = forecast;
}
}
/**
* Get the current time rounded down to hour
*
* @return A {@link ZonedDateTime} corresponding to the last even hour
*/
private ZonedDateTime calculateCurrentHour() {
ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC);
int y = now.getYear();
int m = now.getMonth().getValue();
int d = now.getDayOfMonth();
int h = now.getHour();
return ZonedDateTime.of(y, m, d, h, 0, 0, 0, ZoneOffset.UTC);
}
/**
* Get the current time rounded down to day
*
* @return A {@link ZonedDateTime} corresponding to the last even day.
*/
private ZonedDateTime calculateCurrentDay() {
ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC);
int y = now.getYear();
int m = now.getMonth().getValue();
int d = now.getDayOfMonth();
return ZonedDateTime.of(y, m, d, 0, 0, 0, 0, ZoneOffset.UTC);
}
/**
* Creates channels based on selections in thing configuration
*
* @return
*/
private List<Channel> createChannels() {
List<Channel> channels = new ArrayList<>();
// There's currently a bug in PaperUI that can cause options to be added more than one time
// to the list. Convert to a sorted set to work around this.
// See https://github.com/openhab/openhab-webui/issues/212
Set<Integer> hours = new TreeSet<>();
Set<Integer> days = new TreeSet<>();
if (config.hourlyForecasts != null) {
hours.addAll(config.hourlyForecasts);
}
if (config.dailyForecasts != null) {
days.addAll(config.dailyForecasts);
}
for (int i : hours) {
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "hour_" + i);
CHANNEL_IDS.forEach(id -> {
channels.add(createChannel(groupUID, id));
});
}
for (int i : days) {
ChannelGroupUID groupUID = new ChannelGroupUID(thing.getUID(), "day_" + i);
CHANNEL_IDS.forEach(id -> {
channels.add(createChannel(groupUID, id));
});
}
return channels;
}
/**
* Create a channel with the correct item type based on the channel ID
*
* @param channelGroupUID Channel group the channel belongs to
* @param channelID ID of the channel (without group ID)
* @return The created channel
*/
private Channel createChannel(ChannelGroupUID channelGroupUID, String channelID) {
ChannelUID channelUID = new ChannelUID(channelGroupUID, channelID);
String itemType = "Number";
switch (channelID) {
case TEMPERATURE:
itemType += ":Temperature";
break;
case PRESSURE:
itemType += ":Pressure";
break;
case VISIBILITY:
itemType += ":Length";
break;
case WIND_DIRECTION:
itemType += ":Angle";
case WIND_SPEED:
case GUST:
case PRECIPITATION_MAX:
case PRECIPITATION_MEAN:
case PRECIPITATION_MEDIAN:
case PRECIPITATION_MIN:
itemType += ":Speed";
break;
case RELATIVE_HUMIDITY:
case PERCENT_FROZEN:
case TOTAL_CLOUD_COVER:
case HIGH_CLOUD_COVER:
case MEDIUM_CLOUD_COVER:
case LOW_CLOUD_COVER:
case THUNDER_PROBABILITY:
itemType += ":Dimensionless";
break;
}
Channel channel = ChannelBuilder.create(channelUID, itemType)
.withType(new ChannelTypeUID(BINDING_ID, channelID)).build();
return channel;
}
}

View File

@@ -0,0 +1,67 @@
/**
* 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.smhi.internal;
import static org.openhab.binding.smhi.internal.SmhiBindingConstants.THING_TYPE_FORECAST;
import java.util.Collections;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.core.io.net.http.HttpClientFactory;
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.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* The {@link SmhiHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.smhi", service = ThingHandlerFactory.class)
public class SmhiHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_FORECAST);
private final HttpClient httpClient;
@Activate
public SmhiHandlerFactory(@Reference final HttpClientFactory httpClientFactory) {
this.httpClient = httpClientFactory.getCommonHttpClient();
}
@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_FORECAST.equals(thingTypeUID)) {
return new SmhiHandler(thing, httpClient);
}
return null;
}
}

View File

@@ -0,0 +1,93 @@
/**
* 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.smhi.internal;
import java.time.ZonedDateTime;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* A collection class with utility methods to retrieve forecasts pertaining to a specified time.
*
* @author Anders Alfredsson - Initial contribution
*/
@NonNullByDefault
public class TimeSeries implements Iterable<Forecast> {
private final ZonedDateTime referenceTime;
private final List<Forecast> forecasts;
public TimeSeries(ZonedDateTime referenceTime, List<Forecast> forecasts) {
this.referenceTime = referenceTime;
this.forecasts = forecasts;
}
public ZonedDateTime getReferenceTime() {
return referenceTime;
}
/**
* Retrieves the first {@link Forecast} that is equal to or after offset time (from now).
*
* @param hourOffset number of hours after now.
* @return
*/
public @Nullable Forecast getForecast(int hourOffset) {
return getForecast(ZonedDateTime.now(), hourOffset);
}
/**
* Retrieves the first {@link Forecast} that is equal to or after the offset time (from startTime).
*
* @param hourOffset number of hours after now.
* @return
*/
public @Nullable Forecast getForecast(ZonedDateTime startTime, int hourOffset) {
if (hourOffset < 0) {
throw new IllegalArgumentException("Offset must be at least 0");
}
for (Forecast forecast : forecasts) {
if (forecast.getValidTime().compareTo(startTime.plusHours(hourOffset)) >= 0) {
return forecast;
}
}
return null;
}
@Override
public Iterator<Forecast> iterator() {
return forecasts.iterator();
}
@Override
public void forEach(@Nullable Consumer<? super Forecast> action) {
if (action == null) {
throw new IllegalArgumentException();
}
for (Forecast f : forecasts) {
action.accept(f);
}
}
@Override
public Spliterator<Forecast> spliterator() {
return forecasts.spliterator();
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="smhi" 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>SMHI Binding</name>
<description>Binding for getting weather forecasts from the Swedish Meteorological and Hydrological Institute (SMHI)</description>
<author>Anders Alfredsson</author>
</binding:binding>

View File

@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:smhi:forecast">
<parameter name="latitude" type="decimal" required="true">
<label>Latitude</label>
<description>Latitude for the forecast</description>
</parameter>
<parameter name="longitude" type="decimal" required="true">
<label>Longitude</label>
<description>Longitude for the forecast</description>
</parameter>
<parameter name="hourlyForecasts" type="integer" multiple="true">
<label>Hourly Forecasts</label>
<description>The hourly forecasts to display</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="0">00: Current hour</option>
<option value="1">01: Next hour</option>
<option value="2">02: 2 hours from now</option>
<option value="3">03: 3 hours from now</option>
<option value="4">04: 4 hours from now</option>
<option value="5">05: 5 hours from now</option>
<option value="6">06: 6 hours from now</option>
<option value="7">07: 7 hours from now</option>
<option value="8">08: 8 hours from now</option>
<option value="9">09: 9 hours from now</option>
<option value="10">10: 10 hours from now</option>
<option value="11">11: 11 hours from now</option>
<option value="12">12: 12 hours from now</option>
<option value="13">13: 13 hours from now</option>
<option value="14">14: 14 hours from now</option>
<option value="15">15: 15 hours from now</option>
<option value="16">16: 16 hours from now</option>
<option value="17">17: 17 hours from now</option>
<option value="18">18: 18 hours from now</option>
<option value="19">19: 19 hours from now</option>
<option value="20">20: 20 hours from now</option>
<option value="21">21: 21 hours from now</option>
<option value="22">22: 22 hours from now</option>
<option value="23">23: 23 hours from now</option>
<option value="24">24: 24 hours from now</option>
</options>
</parameter>
<parameter name="dailyForecasts" type="integer" multiple="true">
<label>Daily Forecasts</label>
<description>The daily forecasts to display</description>
<limitToOptions>true</limitToOptions>
<options>
<option value="0">00: Today</option>
<option value="1">01: Tomorrow</option>
<option value="2">02: 2 days from now</option>
<option value="3">03: 3 days from now</option>
<option value="4">04: 4 days from now</option>
<option value="5">05: 5 days from now</option>
<option value="6">06: 6 days from now</option>
<option value="7">07: 7 days from now</option>
<option value="8">08: 8 days from now</option>
<option value="9">09: 9 days from now</option>
</options>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,214 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="smhi"
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">
<channel-type id="msl">
<item-type>Number:Pressure</item-type>
<label>Air Pressure</label>
<description>Air pressure in hPa</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="t">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="vis" advanced="true">
<item-type>Number:Length</item-type>
<label>Visibility</label>
<description>Horizontal visibility</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="wd">
<item-type>Number:Angle</item-type>
<label>Wind Direction</label>
<description>Wind direction</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="ws">
<item-type>Number:Speed</item-type>
<label>Wind Speed</label>
<description>Wind speed</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="r">
<item-type>Number:Dimensionless</item-type>
<label>Relative Humidity</label>
<description>Relative humidity in percent</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="tstm" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Thunder Probability</label>
<description>Probability of thunder in percent</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="tcc_mean">
<item-type>Number:Dimensionless</item-type>
<label>Total Cloud Cover</label>
<description>Mean value of total cloud cover in percent</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="lcc_mean" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Low Level Cloud Cover</label>
<description>Mean value of low level cloud cover (0-2500 m) in percent</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="mcc_mean" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Medium Level Cloud Cover</label>
<description>Mean value of medium level cloud cover (2500-6000 m) in percent</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="hcc_mean" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>High Level Cloud Cover</label>
<description>Mean value of high level cloud cover (> 6000 m) in percent</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="gust">
<item-type>Number:Speed</item-type>
<label>Wind Gust Speed</label>
<description>Wind gust speed</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="pmin">
<item-type>Number:Speed</item-type>
<label>Minimum Precipitation</label>
<description>Minimum precipitation intensity</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="pmax">
<item-type>Number:Speed</item-type>
<label>Maximum Precipitation</label>
<description>Maximum precipitation intensity</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="pmean" advanced="true">
<item-type>Number:Speed</item-type>
<label>Mean Precipitation</label>
<description>Mean precipitation intensity</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="pmedian" advanced="true">
<item-type>Number:Speed</item-type>
<label>Median Precipitation</label>
<description>Median precipitation intensity</description>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<channel-type id="pcat">
<item-type>Number</item-type>
<label>Precipitation Category</label>
<description>Type of precipitation</description>
<state readOnly="true">
<options>
<option value="0">No precipitation</option>
<option value="1">Snow</option>
<option value="2">Snow and rain</option>
<option value="3">Rain</option>
<option value="4">Drizzle</option>
<option value="5">Freezing rain</option>
<option value="6">Freezing drizzle</option>
</options>
</state>
</channel-type>
<channel-type id="spp" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Frozen Precipitation</label>
<description>Percent of precipitation in frozen form</description>
<state readOnly="true" pattern="%d %unit%"/>
</channel-type>
<channel-type id="wsymb2">
<item-type>Number</item-type>
<label>Weather Condition</label>
<description>Short description of the weather conditions</description>
<state readOnly="true">
<options>
<option value="1">Clear sky</option>
<option value="2">Nearly clear sky</option>
<option value="3">Variable cloudiness</option>
<option value="4">Halfclear sky</option>
<option value="5">Cloudy sky</option>
<option value="6">Overcast</option>
<option value="7">Fog</option>
<option value="8">Light rain showers</option>
<option value="9">Moderate rain showers</option>
<option value="10">Heavy rain showers</option>
<option value="11">Thunderstorm</option>
<option value="12">Light sleet showers</option>
<option value="13">Moderate sleet showers</option>
<option value="14">Heavy sleet showers</option>
<option value="15">Light snow showers</option>
<option value="16">Moderate snow showers</option>
<option value="17">Heavy snow showers</option>
<option value="18">Light rain</option>
<option value="19">Moderate rain</option>
<option value="20">Heavy rain</option>
<option value="21">Thunder</option>
<option value="22">Light sleet</option>
<option value="23">Moderate sleet</option>
<option value="24">Heavy sleet</option>
<option value="25">Light snowfall</option>
<option value="26">Moderate snowfall</option>
<option value="27">Heavy snowfall</option>
</options>
</state>
</channel-type>
<channel-group-type id="hourlyForecast">
<label>Hourly Forecast</label>
<description>Hourly forecast for the specified offset</description>
<channels>
<channel id="t" typeId="t"/>
<channel id="wd" typeId="wd"/>
<channel id="ws" typeId="ws"/>
<channel id="gust" typeId="gust"/>
<channel id="pmin" typeId="pmin"/>
<channel id="pmax" typeId="pmax"/>
<channel id="pcat" typeId="pcat"/>
<channel id="msl" typeId="msl"/>
<channel id="r" typeId="r"/>
<channel id="tcc_mean" typeId="tcc_mean"/>
<channel id="wsymb2" typeId="wsymb2"/>
<channel id="vis" typeId="vis"/>
<channel id="tstm" typeId="tstm"/>
<channel id="spp" typeId="spp"/>
<channel id="lcc_mean" typeId="lcc_mean"/>
<channel id="mcc_mean" typeId="mcc_mean"/>
<channel id="hcc_mean" typeId="hcc_mean"/>
<channel id="pmean" typeId="pmean"/>
<channel id="pmedian" typeId="pmedian"/>
</channels>
</channel-group-type>
<channel-group-type id="dailyForecast">
<label>Daily Forecast</label>
<description>Forecast at noon for the specified offset</description>
<channels>
<channel id="t" typeId="t"/>
<channel id="wd" typeId="wd"/>
<channel id="ws" typeId="ws"/>
<channel id="gust" typeId="gust"/>
<channel id="pmin" typeId="pmin"/>
<channel id="pmax" typeId="pmax"/>
<channel id="pcat" typeId="pcat"/>
<channel id="msl" typeId="msl"/>
<channel id="r" typeId="r"/>
<channel id="tcc_mean" typeId="tcc_mean"/>
<channel id="wsymb2" typeId="wsymb2"/>
<channel id="vis" typeId="vis"/>
<channel id="tstm" typeId="tstm"/>
<channel id="spp" typeId="spp"/>
<channel id="lcc_mean" typeId="lcc_mean"/>
<channel id="mcc_mean" typeId="mcc_mean"/>
<channel id="hcc_mean" typeId="hcc_mean"/>
<channel id="pmean" typeId="pmean"/>
<channel id="pmedian" typeId="pmedian"/>
</channels>
</channel-group-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,157 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="smhi"
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">
<thing-type id="forecast">
<label>SMHI Weather Forecast</label>
<description>Gets weather forecasts from SMHI</description>
<channel-groups>
<channel-group id="hour_0" typeId="hourlyForecast">
<label>Current Hour</label>
<description>Forecast for the current hour</description>
</channel-group>
<channel-group id="hour_1" typeId="hourlyForecast">
<label>Next Hour</label>
<description>Forecast for the next hour</description>
</channel-group>
<channel-group id="hour_2" typeId="hourlyForecast">
<label>2 Hours from Now</label>
<description>Forecast for 2 hours from now</description>
</channel-group>
<channel-group id="hour_3" typeId="hourlyForecast">
<label>3 Hours from Now</label>
<description>Forecast for 3 hours from now</description>
</channel-group>
<channel-group id="hour_4" typeId="hourlyForecast">
<label>4 Hours from Now</label>
<description>Forecast for 4 hours from now</description>
</channel-group>
<channel-group id="hour_5" typeId="hourlyForecast">
<label>5 Hours from Now</label>
<description>Forecast for 5 hours from now</description>
</channel-group>
<channel-group id="hour_6" typeId="hourlyForecast">
<label>6 Hours from Now</label>
<description>Forecast for 6 hours from now</description>
</channel-group>
<channel-group id="hour_7" typeId="hourlyForecast">
<label>7 Hours from Now</label>
<description>Forecast for 7 hours from now</description>
</channel-group>
<channel-group id="hour_8" typeId="hourlyForecast">
<label>8 Hours from Now</label>
<description>Forecast for 8 hours from now</description>
</channel-group>
<channel-group id="hour_9" typeId="hourlyForecast">
<label>9 Hours from Now</label>
<description>Forecast for 9 hours from now</description>
</channel-group>
<channel-group id="hour_10" typeId="hourlyForecast">
<label>10 Hours from Now</label>
<description>Forecast for 10 hours from now</description>
</channel-group>
<channel-group id="hour_11" typeId="hourlyForecast">
<label>11 Hours from Now</label>
<description>Forecast for 11 hours from now</description>
</channel-group>
<channel-group id="hour_12" typeId="hourlyForecast">
<label>12 Hours from Now</label>
<description>Forecast for 12 hours from now</description>
</channel-group>
<channel-group id="hour_13" typeId="hourlyForecast">
<label>13 Hours from Now</label>
<description>Forecast for 13 hours from now</description>
</channel-group>
<channel-group id="hour_14" typeId="hourlyForecast">
<label>14 Hours from Now</label>
<description>Forecast for 14 hours from now</description>
</channel-group>
<channel-group id="hour_15" typeId="hourlyForecast">
<label>15 Hours from Now</label>
<description>Forecast for 15 hours from now</description>
</channel-group>
<channel-group id="hour_16" typeId="hourlyForecast">
<label>16 Hours from Now</label>
<description>Forecast for 16 hours from now</description>
</channel-group>
<channel-group id="hour_17" typeId="hourlyForecast">
<label>17 Hours from Now</label>
<description>Forecast for 17 hours from now</description>
</channel-group>
<channel-group id="hour_18" typeId="hourlyForecast">
<label>18 Hours from Now</label>
<description>Forecast for 18 hours from now</description>
</channel-group>
<channel-group id="hour_19" typeId="hourlyForecast">
<label>19 Hours from Now</label>
<description>Forecast for 19 hours from now</description>
</channel-group>
<channel-group id="hour_20" typeId="hourlyForecast">
<label>20 Hours from Now</label>
<description>Forecast for 20 hours from now</description>
</channel-group>
<channel-group id="hour_21" typeId="hourlyForecast">
<label>21 Hours from Now</label>
<description>Forecast for 21 hours from now</description>
</channel-group>
<channel-group id="hour_22" typeId="hourlyForecast">
<label>22 Hours from Now</label>
<description>Forecast for 22 hours from now</description>
</channel-group>
<channel-group id="hour_23" typeId="hourlyForecast">
<label>23 Hours from Now</label>
<description>Forecast for 23 hours from now</description>
</channel-group>
<channel-group id="hour_24" typeId="hourlyForecast">
<label>24 Hours from Now</label>
<description>Forecast for 24 hours from now</description>
</channel-group>
<channel-group id="day_0" typeId="dailyForecast">
<label>Today</label>
<description>Forecast for today</description>
</channel-group>
<channel-group id="day_1" typeId="dailyForecast">
<label>Tomorrow</label>
<description>Forecast for tomorrow</description>
</channel-group>
<channel-group id="day_2" typeId="dailyForecast">
<label>2 Days from Now</label>
<description>Forecast for 2 days from now</description>
</channel-group>
<channel-group id="day_3" typeId="dailyForecast">
<label>3 Days from Now</label>
<description>Forecast for 3 days from now</description>
</channel-group>
<channel-group id="day_4" typeId="dailyForecast">
<label>4 Days from Now</label>
<description>Forecast for 4 days from now</description>
</channel-group>
<channel-group id="day_5" typeId="dailyForecast">
<label>5 Days from Now</label>
<description>Forecast for 5 days from now</description>
</channel-group>
<channel-group id="day_6" typeId="dailyForecast">
<label>6 Days from Now</label>
<description>Forecast for 6 days from now</description>
</channel-group>
<channel-group id="day_7" typeId="dailyForecast">
<label>7 Days from Now</label>
<description>Forecast for 7 days from now</description>
</channel-group>
<channel-group id="day_8" typeId="dailyForecast">
<label>8 Days from Now</label>
<description>Forecast for 8 days from now</description>
</channel-group>
<channel-group id="day_9" typeId="dailyForecast">
<label>9 Days from Now</label>
<description>Forecast for 9 days from now</description>
</channel-group>
</channel-groups>
<config-description-ref uri="thing-type:smhi:forecast"/>
</thing-type>
</thing:thing-descriptions>