[Sagercaster] Reintroducing timestamp channel (#11665)

[Sagercaster] Reintroducing timestamp channel 

Signed-off-by: clinique <gael@lhopital.org>
pull/11706/head
Gaël L'hopital 2021-12-04 16:55:48 +01:00 committed by GitHub
parent 13bae622ac
commit 4605edeb29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 190 additions and 131 deletions

View File

@ -6,7 +6,8 @@ The Sager Weathercaster is a scientific instrument for accurate prediction of th
* To operate, this binding will need to use channel values provided by other means (e.g. Weather Binding, Netatmo, a 1-Wire personal weather station...)
* This binding buffers readings for some hours before producing weather forecasts(wind direction and sea level pressure). SagerWeatherCaster needs an observation period of minimum 6 hours.
* This binding buffers readings for some hours before producing weather forecasts(wind direction and sea level pressure).
SagerWeatherCaster needs an observation period of minimum 6 hours.
For these reasons, this binding is not a binding in the usual sense.
@ -24,9 +25,11 @@ The binding itself does not require any configuration.
| Name | Type | Description |
|--------------------|----------|--------------------------------------------------------------------------|
| location | Location | Latitude and longitude of the desired weather forecast. |
| location (*) | Location | Latitude and longitude of the desired weather forecast. |
| observation-period | int | Minimum delay (in hours) before producing forecasts. Defaulted to 6. |
(*) Only latitude is used by the algorithm.
## Channels
The binding will use some input channels, that can be configured directly with profiles (sample below).
@ -41,6 +44,8 @@ The binding will use some input channels, that can be configured directly with p
| wind-speed-beaufort | input |Number | Wind speed expressed using the Beaufort scale |
| pressure | input |Number:Pressure | Sea level pressure |
| wind-angle | input |Number:Angle | Wind direction |
| temperature | input |Number:Temperature | Outside temperature |
| timestamp | output |DateTime | Timestamp of the last forecast update |
| forecast | output |String | Description of the weather forecast |
| velocity | output |String | Description of the expected wind evolution |
| velocity-beaufort | output |Number | Expected wind evolution using the Beaufort scale |

View File

@ -48,6 +48,8 @@ public class SagerCasterBindingConstants {
public static final String CHANNEL_WINDEVOLUTION = "wind-evolution";
public static final String CHANNEL_PRESSURETREND = "pressure-trend";
public static final String CHANNEL_TEMPERATURETREND = "temperature-trend";
public static final String CHANNEL_TIMESTAMP = "timestamp";
// Input channel ids
public static final String CHANNEL_CLOUDINESS = "cloudiness";
public static final String CHANNEL_IS_RAINING = "is-raining";

View File

@ -19,6 +19,7 @@ import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.sagercaster.internal.caster.SagerWeatherCaster;
import org.openhab.binding.sagercaster.internal.handler.SagerCasterHandler;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) 2010-2021 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.sagercaster.internal.caster;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* This class holds the result of the SagerCaster algorithm
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class SagerPrediction {
private final String prediction;
public SagerPrediction(String sagerCode) {
this.prediction = sagerCode;
}
public String getSagerCode() {
return prediction;
}
public String getForecast() {
return Character.toString(prediction.charAt(0));
}
public String getWindVelocity() {
return Character.toString(prediction.charAt(1));
}
public String getWindDirection() {
return Character.toString(prediction.charAt(2));
}
public String getWindDirection2() {
return prediction.length() > 3 ? Character.toString(prediction.charAt(3)) : SagerWeatherCaster.UNDEF;
}
}

View File

@ -53,7 +53,7 @@
* 378 possible forecasts determined from 4996 dial codes.
*/
package org.openhab.binding.sagercaster.internal;
package org.openhab.binding.sagercaster.internal.caster;
import java.io.IOException;
import java.io.InputStream;
@ -76,6 +76,7 @@ import org.slf4j.LoggerFactory;
@Component(service = SagerWeatherCaster.class, scope = ServiceScope.SINGLETON)
@NonNullByDefault
public class SagerWeatherCaster {
public final static String UNDEF = "-";
// Northern Polar Zone & Northern Tropical Zone
private final static String[] NPZDIRECTIONS = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" };
// Northern Temperate Zone
@ -88,7 +89,7 @@ public class SagerWeatherCaster {
private final Logger logger = LoggerFactory.getLogger(SagerWeatherCaster.class);
private final Properties forecaster = new Properties();
private Optional<Prevision> prevision = Optional.empty();
private Optional<SagerPrediction> prevision = Optional.empty();
private String[] usedDirections = NTZDIRECTIONS; // Defaulted to Northern Zone
private int currentBearing = -1;
@ -238,7 +239,7 @@ public class SagerWeatherCaster {
private void updatePrediction() {
int zWind = Arrays.asList(usedDirections).indexOf(getCompass());
String d1 = "-";
String d1 = UNDEF;
switch (zWind) {
case 0:
if (windEvolution == 3) {
@ -319,39 +320,27 @@ public class SagerWeatherCaster {
}
String forecast = forecaster.getProperty(
d1 + String.valueOf(sagerPressure) + String.valueOf(pressureEvolution) + String.valueOf(nubes));
prevision = (forecast != null) ? Optional.of(new Prevision(forecast)) : Optional.empty();
prevision = Optional.ofNullable(forecast != null ? new SagerPrediction(forecast) : null);
}
public String getForecast() {
if (prevision.isPresent()) {
char forecast = prevision.get().zForecast;
return Character.toString(forecast);
}
return "-";
return prevision.map(p -> p.getForecast()).orElse(UNDEF);
}
public String getWindVelocity() {
if (prevision.isPresent()) {
char windVelocity = prevision.get().zWindVelocity;
return Character.toString(windVelocity);
}
return "-";
return prevision.map(p -> p.getWindVelocity()).orElse(UNDEF);
}
public String getWindDirection() {
if (prevision.isPresent()) {
int direction = prevision.get().zWindDirection;
return String.valueOf(direction);
}
return "-";
return prevision.map(p -> p.getWindDirection()).orElse(UNDEF);
}
public String getWindDirection2() {
if (prevision.isPresent()) {
int direction = prevision.get().zWindDirection2;
return String.valueOf(direction);
}
return "-";
return prevision.map(p -> p.getWindDirection2()).orElse(UNDEF);
}
public String getSagerCode() {
return prevision.map(p -> p.getSagerCode()).orElse(UNDEF);
}
public void setLatitude(double latitude) {
@ -370,17 +359,31 @@ public class SagerWeatherCaster {
}
}
private class Prevision {
public final char zForecast;
public final char zWindVelocity;
public final int zWindDirection;
public final int zWindDirection2;
public Prevision(String forecast) {
zForecast = forecast.charAt(0);
zWindVelocity = forecast.charAt(1);
zWindDirection = Character.getNumericValue(forecast.charAt(2));
zWindDirection2 = (forecast.length() > 3) ? Character.getNumericValue(forecast.charAt(3)) : -1;
public int getPredictedBeaufort() {
int result = currentBeaufort;
switch (getWindVelocity()) {
case "N":
result += 1;
break;
case "F":
result = 4;
break;
case "S":
result = 6;
break;
case "G":
result = 8;
break;
case "W":
result = 10;
break;
case "H":
result = 12;
break;
case "D":
result -= 1;
break;
}
return result;
}
}

View File

@ -16,9 +16,9 @@ import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstan
import static org.openhab.core.library.unit.MetricPrefix.HECTO;
import java.math.BigDecimal;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.measure.quantity.Angle;
@ -26,8 +26,9 @@ import javax.measure.quantity.Pressure;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.sagercaster.internal.SagerWeatherCaster;
import org.openhab.binding.sagercaster.internal.WindDirectionStateDescriptionProvider;
import org.openhab.binding.sagercaster.internal.caster.SagerWeatherCaster;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
@ -36,6 +37,7 @@ import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
@ -58,11 +60,12 @@ public class SagerCasterHandler extends BaseThingHandler {
private final WindDirectionStateDescriptionProvider stateDescriptionProvider;
private final ExpiringMap<QuantityType<Pressure>> pressureCache = new ExpiringMap<>();
private final ExpiringMap<QuantityType<Temperature>> temperatureCache = new ExpiringMap<>();
private final ExpiringMap<QuantityType<Angle>> bearingCache = new ExpiringMap<>();
private final ExpiringMap<Double> pressureCache = new ExpiringMap<>();
private final ExpiringMap<Double> temperatureCache = new ExpiringMap<>();
private final ExpiringMap<Integer> bearingCache = new ExpiringMap<>();
private int currentTemp = 0;
private double currentTemp = 0;
private String currentSagerCode = SagerWeatherCaster.UNDEF;
public SagerCasterHandler(Thing thing, WindDirectionStateDescriptionProvider stateDescriptionProvider,
SagerWeatherCaster sagerWeatherCaster) {
@ -73,15 +76,17 @@ public class SagerCasterHandler extends BaseThingHandler {
@Override
public void initialize() {
String location = (String) getConfig().get(CONFIG_LOCATION);
int observationPeriod = ((BigDecimal) getConfig().get(CONFIG_PERIOD)).intValue();
String latitude = location.split(",")[0];
sagerWeatherCaster.setLatitude(Double.parseDouble(latitude));
long period = TimeUnit.HOURS.toMillis(observationPeriod);
pressureCache.setObservationPeriod(period);
bearingCache.setObservationPeriod(period);
temperatureCache.setObservationPeriod(period);
String location = (String) getConfig().get(CONFIG_LOCATION);
String latitude = location.split(",")[0];
sagerWeatherCaster.setLatitude(Double.parseDouble(latitude));
defineWindDirectionStateDescriptions();
updateStatus(ThingStatus.ONLINE);
}
@ -95,10 +100,9 @@ public class SagerCasterHandler extends BaseThingHandler {
}
options.add(new StateOption("9", "Shifting / Variable winds"));
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), GROUP_OUTPUT, CHANNEL_WINDFROM),
options);
stateDescriptionProvider.setStateOptions(new ChannelUID(getThing().getUID(), GROUP_OUTPUT, CHANNEL_WINDTO),
options);
ThingUID thingUID = getThing().getUID();
stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, GROUP_OUTPUT, CHANNEL_WINDFROM), options);
stateDescriptionProvider.setStateOptions(new ChannelUID(thingUID, GROUP_OUTPUT, CHANNEL_WINDTO), options);
}
@Override
@ -109,7 +113,7 @@ public class SagerCasterHandler extends BaseThingHandler {
String id = channelUID.getIdWithoutGroup();
switch (id) {
case CHANNEL_CLOUDINESS:
logger.debug("Octa cloud level changed, updating forecast");
logger.debug("Cloud level changed, updating forecast");
if (command instanceof QuantityType) {
QuantityType<?> cloudiness = (QuantityType<?>) command;
scheduler.submit(() -> {
@ -127,26 +131,17 @@ public class SagerCasterHandler extends BaseThingHandler {
postNewForecast();
});
} else {
logger.debug("Channel '{}' can only accept Switch type commands.", channelUID);
logger.debug("Channel '{}' accepts Switch commands.", channelUID);
}
break;
case CHANNEL_RAIN_QTTY:
logger.debug("Rain status updated, updating forecast");
if (command instanceof QuantityType) {
QuantityType<?> newQtty = (QuantityType<?>) command;
scheduler.submit(() -> {
sagerWeatherCaster.setRaining(newQtty.doubleValue() > 0);
postNewForecast();
});
updateRain((QuantityType<?>) command);
} else if (command instanceof DecimalType) {
DecimalType newQtty = (DecimalType) command;
scheduler.submit(() -> {
sagerWeatherCaster.setRaining(newQtty.doubleValue() > 0);
postNewForecast();
});
updateRain((DecimalType) command);
} else {
logger.debug("Channel '{}' can accept Number, Number:Speed, Number:Length type commands.",
channelUID);
logger.debug("Channel '{}' accepts Number, Number:(Speed|Length) commands.", channelUID);
}
break;
case CHANNEL_WIND_SPEED:
@ -165,12 +160,13 @@ public class SagerCasterHandler extends BaseThingHandler {
logger.debug("Sea-level pressure updated, updating forecast");
if (command instanceof QuantityType) {
@SuppressWarnings("unchecked")
QuantityType<Pressure> newPressure = ((QuantityType<Pressure>) command)
QuantityType<Pressure> pressQtty = ((QuantityType<Pressure>) command)
.toUnit(HECTO(SIUnits.PASCAL));
if (newPressure != null) {
pressureCache.put(newPressure);
pressureCache.getAgedValue().ifPresentOrElse(pressure -> scheduler.submit(() -> {
sagerWeatherCaster.setPressure(newPressure.doubleValue(), pressure.doubleValue());
if (pressQtty != null) {
double newPressureValue = pressQtty.doubleValue();
pressureCache.put(newPressureValue);
pressureCache.getAgedValue().ifPresentOrElse(oldPressure -> scheduler.submit(() -> {
sagerWeatherCaster.setPressure(newPressureValue, oldPressure);
updateChannelString(GROUP_OUTPUT, CHANNEL_PRESSURETREND,
String.valueOf(sagerWeatherCaster.getPressureEvolution()));
postNewForecast();
@ -182,14 +178,13 @@ public class SagerCasterHandler extends BaseThingHandler {
logger.debug("Temperature updated");
if (command instanceof QuantityType) {
@SuppressWarnings("unchecked")
QuantityType<Temperature> newTemperature = ((QuantityType<Temperature>) command)
QuantityType<Temperature> tempQtty = ((QuantityType<Temperature>) command)
.toUnit(SIUnits.CELSIUS);
if (newTemperature != null) {
temperatureCache.put(newTemperature);
currentTemp = newTemperature.intValue();
Optional<QuantityType<Temperature>> agedTemperature = temperatureCache.getAgedValue();
agedTemperature.ifPresent(temperature -> {
double delta = newTemperature.doubleValue() - temperature.doubleValue();
if (tempQtty != null) {
currentTemp = tempQtty.doubleValue();
temperatureCache.put(currentTemp);
temperatureCache.getAgedValue().ifPresent(oldTemperature -> {
double delta = currentTemp - oldTemperature;
String trend = (delta > 3) ? "1"
: (delta > 0.3) ? "2" : (delta > -0.3) ? "3" : (delta > -3) ? "4" : "5";
updateChannelString(GROUP_OUTPUT, CHANNEL_TEMPERATURETREND, trend);
@ -201,12 +196,12 @@ public class SagerCasterHandler extends BaseThingHandler {
logger.debug("Updated wind direction, updating forecast");
if (command instanceof QuantityType) {
@SuppressWarnings("unchecked")
QuantityType<Angle> newAngle = (QuantityType<Angle>) command;
bearingCache.put(newAngle);
Optional<QuantityType<Angle>> agedAngle = bearingCache.getAgedValue();
agedAngle.ifPresent(angle -> {
QuantityType<Angle> angleQtty = (QuantityType<Angle>) command;
int newAngleValue = angleQtty.intValue();
bearingCache.put(newAngleValue);
bearingCache.getAgedValue().ifPresent(oldAngle -> {
scheduler.submit(() -> {
sagerWeatherCaster.setBearing(newAngle.intValue(), angle.intValue());
sagerWeatherCaster.setBearing(newAngleValue, oldAngle);
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDEVOLUTION,
String.valueOf(sagerWeatherCaster.getWindEvolution()));
postNewForecast();
@ -220,42 +215,36 @@ public class SagerCasterHandler extends BaseThingHandler {
}
}
private void updateRain(Number newQtty) {
scheduler.submit(() -> {
sagerWeatherCaster.setRaining(newQtty.doubleValue() > 0);
postNewForecast();
});
}
private void postNewForecast() {
String forecast = sagerWeatherCaster.getForecast();
// Sharpens forecast if current temp is below 2 degrees, likely to be flurries rather than shower
forecast += SHOWERS.contains(forecast) ? (currentTemp > 2) ? "1" : "2" : "";
String newSagerCode = sagerWeatherCaster.getSagerCode();
if (!newSagerCode.equals(currentSagerCode)) {
logger.debug("Sager prediction changed to {}", newSagerCode);
currentSagerCode = newSagerCode;
updateChannelTimeStamp(GROUP_OUTPUT, CHANNEL_TIMESTAMP, ZonedDateTime.now());
String forecast = sagerWeatherCaster.getForecast();
// Sharpens forecast if current temp is below 2 degrees, likely to be flurries rather than shower
forecast += SHOWERS.contains(forecast) ? (currentTemp > 2) ? "1" : "2" : "";
updateChannelString(GROUP_OUTPUT, CHANNEL_FORECAST, forecast);
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDFROM, sagerWeatherCaster.getWindDirection());
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDTO, sagerWeatherCaster.getWindDirection2());
String velocity = sagerWeatherCaster.getWindVelocity();
updateChannelString(GROUP_OUTPUT, CHANNEL_VELOCITY, velocity);
int predictedBeaufort = sagerWeatherCaster.getBeaufort();
switch (velocity) {
case "N":
predictedBeaufort += 1;
break;
case "F":
predictedBeaufort = 4;
break;
case "S":
predictedBeaufort = 6;
break;
case "G":
predictedBeaufort = 8;
break;
case "W":
predictedBeaufort = 10;
break;
case "H":
predictedBeaufort = 12;
break;
case "D":
predictedBeaufort -= 1;
break;
updateChannelString(GROUP_OUTPUT, CHANNEL_FORECAST, forecast);
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDFROM, sagerWeatherCaster.getWindDirection());
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDTO, sagerWeatherCaster.getWindDirection2());
updateChannelString(GROUP_OUTPUT, CHANNEL_VELOCITY, sagerWeatherCaster.getWindVelocity());
updateChannelDecimal(GROUP_OUTPUT, CHANNEL_VELOCITY_BEAUFORT, sagerWeatherCaster.getPredictedBeaufort());
}
}
private void updateChannelTimeStamp(String group, String channelId, ZonedDateTime zonedDateTime) {
ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
if (isLinked(id)) {
updateState(id, new DateTimeType(zonedDateTime));
}
updateChannelDecimal(GROUP_OUTPUT, CHANNEL_VELOCITY_BEAUFORT, predictedBeaufort);
}
private void updateChannelString(String group, String channelId, String value) {

View File

@ -43,6 +43,8 @@ rainingDescription = Is it currently raining ?
beaufortLabel = Beaufort
beaufortDescription = Wind speed using Beaufort Scale
pressureDescription = Barometric pressure at sea level.
timestampChannelLabel = Timestamp
timestampChannelDescription = Timestamp of the last weather forecast update.
# channel options
forecast0 = Not enough historic data to study pressure evolution, wait a bit ...
@ -52,33 +54,33 @@ forecastC = Fair and cooler
forecastD = Unsettled
forecastE = Unsettled and warmer
forecastF = Unsettled and cooler
forecastG = Increasing cloudiness or overcast followed by Precipitation or showers/Flurries
forecastG1 = Increasing cloudiness or overcast followed by Precipitation or showers
forecastG2 = Increasing cloudiness or overcast followed by Precipitation or Flurries
forecastH = Increasing cloudiness or overcast followed by Precipitation or showers and warmer
forecastG = Increasing cloudiness or overcast followed by precipitation or showers/flurries
forecastG1 = Increasing cloudiness or overcast followed by precipitation or showers
forecastG2 = Increasing cloudiness or overcast followed by precipitation or flurries
forecastH = Increasing cloudiness or overcast followed by precipitation or showers and warmer
forecastJ = Showers
forecastK = Showers/Flurries and warmer
forecastK = Showers/flurries and warmer
forecastK1 = Showers and warmer
forecastK2 = Flurries and warmer
forecastL = Showers/Flurries and cooler
forecastL = Showers/flurries and cooler
forecastL1 = Showers and cooler
forecastL2 = Flurries and cooler
forecastM = Precipitation
forecastN = Precipitation and warmer
forecastP = Precipitation and turning cooler; then improvement likely in 24 hours
forecastR = Precipitation or showers/Flurries followed by improvement (within 12 hours)
forecastR = Precipitation or showers/flurries followed by improvement (within 12 hours)
forecastR1 = Precipitation or showers followed by improvement (within 12 hours)
forecastR2 = Precipitation or flurries followed by improvement (within 12 hours)
forecastS = Precipitation or showers/Flurries followed by improvement (within 12 hours) and becoming cooler
forecastS = Precipitation or showers/flurries followed by improvement (within 12 hours) and becoming cooler
forecastS1 = Precipitation or showers followed by improvement (within 12 hours) and becoming cooler
forecastS2 = Precipitation or flurries followed by improvement (within 12 hours) and becoming cooler
forecastT = Precipitation or showers/Flurries followed by improvement early in period (within 6 hours)
forecastT = Precipitation or showers/flurries followed by improvement early in period (within 6 hours)
forecastT1 = Precipitation or showers followed by improvement early in period (within 6 hours)
forecastT2 = Precipitation or flurries followed by improvement early in period (within 6 hours)
forecastU = Precipitation or showers/Flurries by improvement early in period (within 6 hours) and becoming cooler
forecastU = Precipitation or showers/flurries by improvement early in period (within 6 hours) and becoming cooler
forecastU1 = Precipitation or showers by improvement early in period (within 6 hours) and becoming cooler
forecastU2 = Precipitation or flurries by improvement early in period (within 6 hours) and becoming cooler
forecastW = Precipitation or showers/Flurries followed by fair early in period (within 6 hours) and becoming cooler
forecastW = Precipitation or showers/flurries followed by fair early in period (within 6 hours) and becoming cooler
forecastW1 = Precipitation or showers followed by fair early in period (within 6 hours) and becoming cooler
forecastW2 = Precipitation or flurries followed by fair early in period (within 6 hours) and becoming cooler
forecastX = Unsettled followed by fair

View File

@ -43,6 +43,8 @@ rainingDescription = Pleut-il actuellement ?
beaufortLabel = Beaufort
beaufortDescription = Force du vent mesurée sur l'échelle Beaufort
pressureDescription = Pression barométrique au niveau de la mer.
timestampChannelLabel = Horodatage
timestampChannelDescription = Horodatage de la dernière mise à jour de la prévision météo.
# channel options
forecast0 = Patientez encore un peu pour une prédiction

View File

@ -72,6 +72,7 @@
<label>@text/tempTrendLabel</label>
<description>@text/tempTrendDescription</description>
</channel>
<channel id="timestamp" typeId="timestamp"/>
</channels>
</channel-group-type>
@ -163,6 +164,7 @@
<item-type>String</item-type>
<label>@text/trendLabel</label>
<description>@text/trendDescription</description>
<category>Line</category>
<state readOnly="true" pattern="%s">
<options>
<option value="1">@text/trend1</option>
@ -174,19 +176,23 @@
</state>
</channel-type>
<channel-type id="timestamp" advanced="true">
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>@text/timestampLabel</label>
<description>@text/timestampDescription</description>
<category>Observation time</category>
<state readOnly="true"></state>
<label>@text/timestampChannelLabel</label>
<description>@text/timestampChannelDescription</description>
<category>Time</category>
<tags>
<tag>Status</tag>
<tag>Timestamp</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="cloudiness">
<item-type>Number:Dimensionless</item-type>
<label>@text/cloudinessLabel</label>
<description>@text/cloudinessDescription</description>
<category>Clouds</category>
<category>Sun_Clouds</category>
<state min="0" max="100" pattern="%d %%"/>
</channel-type>