added migrated 2.x add-ons
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<features name="org.openhab.binding.sagercaster-${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-sagercaster" description="Sager Weathercaster Binding" version="${project.version}">
|
||||
<feature>openhab-runtime-base</feature>
|
||||
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.sagercaster/${project.version}</bundle>
|
||||
</feature>
|
||||
</features>
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 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.sagercaster.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
/**
|
||||
* The {@link SagerCasterBindingConstants} class defines common constants, which are
|
||||
* used across the whole binding.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SagerCasterBindingConstants {
|
||||
|
||||
public static final String BINDING_ID = "sagercaster";
|
||||
public static final String LOCAL = "local";
|
||||
|
||||
// List of all Thing Type UIDs
|
||||
public static final ThingTypeUID THING_TYPE_SAGERCASTER = new ThingTypeUID(BINDING_ID, "sagercaster");
|
||||
|
||||
// Configuration elements
|
||||
public static final String CONFIG_LOCATION = "location";
|
||||
public static final String CONFIG_PERIOD = "observation-period";
|
||||
|
||||
// List of all Channel Groups Group Channel ids
|
||||
public static final String GROUP_INPUT = "input";
|
||||
public static final String GROUP_OUTPUT = "output";
|
||||
|
||||
// Output channel ids
|
||||
public static final String CHANNEL_FORECAST = "forecast";
|
||||
public static final String CHANNEL_VELOCITY = "velocity";
|
||||
public static final String CHANNEL_VELOCITY_BEAUFORT = "velocity-beaufort";
|
||||
public static final String CHANNEL_WINDFROM = "wind-from";
|
||||
public static final String CHANNEL_WINDTO = "wind-to";
|
||||
public static final String CHANNEL_WINDEVOLUTION = "wind-evolution";
|
||||
public static final String CHANNEL_PRESSURETREND = "pressure-trend";
|
||||
public static final String CHANNEL_TEMPERATURETREND = "temperature-trend";
|
||||
// Input channel ids
|
||||
public static final String CHANNEL_CLOUDINESS = "cloudiness";
|
||||
public static final String CHANNEL_IS_RAINING = "is-raining";
|
||||
public static final String CHANNEL_RAIN_QTTY = "rain-qtty";
|
||||
public static final String CHANNEL_WIND_SPEED = "wind-speed-beaufort";
|
||||
public static final String CHANNEL_TEMPERATURE = "temperature";
|
||||
public static final String CHANNEL_PRESSURE = "pressure";
|
||||
public static final String CHANNEL_WIND_ANGLE = "wind-angle";
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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.sagercaster.internal;
|
||||
|
||||
import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstants.THING_TYPE_SAGERCASTER;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.binding.sagercaster.internal.handler.SagerCasterHandler;
|
||||
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 SagerCasterHandlerFactory} is responsible for creating things and thing
|
||||
* handlers.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.sagercaster")
|
||||
@NonNullByDefault
|
||||
public class SagerCasterHandlerFactory extends BaseThingHandlerFactory {
|
||||
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_SAGERCASTER);
|
||||
private final WindDirectionStateDescriptionProvider stateDescriptionProvider;
|
||||
private final SagerWeatherCaster sagerWeatherCaster;
|
||||
|
||||
@Activate
|
||||
public SagerCasterHandlerFactory(@Reference SagerWeatherCaster sagerWeatherCaster,
|
||||
@Reference WindDirectionStateDescriptionProvider provider) {
|
||||
this.stateDescriptionProvider = provider;
|
||||
this.sagerWeatherCaster = sagerWeatherCaster;
|
||||
}
|
||||
|
||||
@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 (thingTypeUID.equals(THING_TYPE_SAGERCASTER)) {
|
||||
return new SagerCasterHandler(thing, stateDescriptionProvider, sagerWeatherCaster);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
/**
|
||||
* eltiempo.selfip.com - Sager Weathercaster Algorhithm
|
||||
*
|
||||
* Copyright © 2008 Naish666 (eltiempo.selfip.com)
|
||||
* October 2008 - v1.0
|
||||
* Java transposition done by Gaël L'hopital - 2015
|
||||
**
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
***
|
||||
* BT's Global Sager Weathercaster PHP Scripts For Cumulus (Weathercaster)
|
||||
* by "Buford T. Justice" / "BTJustice"
|
||||
* http://www.freewebs.com/btjustice/bt-forecasters.html
|
||||
* 2014-02-05
|
||||
*
|
||||
* You may redistribute and use these PHP Scripts any way you wish as long as
|
||||
* they remain FREE and money is not charged for their use directly or indirectly.
|
||||
* If these PHP Scripts are used in your work or are modified in any way, please
|
||||
* retain the full credit header.
|
||||
* Based Upon:
|
||||
* The Sager Weathercaster: A Scientific Instrument for Accurate Prediction of
|
||||
* the Weather
|
||||
* Copyright © 1969 by Raymond M. Sager and E. F. Sager
|
||||
" The Sager Weathercaster predicts the weather quickly and accurately. It has been
|
||||
* in use since 1942.
|
||||
* Not a novelty, not a toy, this is a highly dependable, scientifically designed
|
||||
* tool of inestimable value to travelers, farmers, hunters, sailors, yachtsmen, campers,
|
||||
* fishermen, students -- in fact, to everyone who needs or wants to know what
|
||||
* the weather will be."
|
||||
* 378 possible forecasts determined from 4996 dial codes.
|
||||
*/
|
||||
|
||||
package org.openhab.binding.sagercaster.internal;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.osgi.service.component.annotations.Activate;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
import org.osgi.service.component.annotations.ServiceScope;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This class is responsible for handling the SagerWeatherCaster algorithm
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@Component(service = SagerWeatherCaster.class, scope = ServiceScope.SINGLETON)
|
||||
@NonNullByDefault
|
||||
public class SagerWeatherCaster {
|
||||
private final Properties forecaster = new Properties();
|
||||
|
||||
// Northern Polar Zone & Northern Tropical Zone
|
||||
private final static String[] NPZDIRECTIONS = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" };
|
||||
// Northern Temperate Zone
|
||||
private final static String[] NTZDIRECTIONS = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
|
||||
// Southern Polar Zone & Southern Tropical Zone
|
||||
private final static String[] SPZDIRECTIONS = { "N", "NW", "W", "SW", "S", "SE", "E", "NE" };
|
||||
// Southern Temperate Zone
|
||||
private final static String[] STZDIRECTIONS = { "S", "SE", "E", "NE", "N", "NW", "W", "SW" };
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SagerWeatherCaster.class);
|
||||
|
||||
private Optional<Prevision> prevision = Optional.empty();
|
||||
private @NonNullByDefault({}) String[] usedDirections;
|
||||
|
||||
private int currentBearing = -1;
|
||||
private int windEvolution = -1; // Whether the wind during the last 6 hours has changed its direction by
|
||||
// approximately 45 degrees or more
|
||||
private int sagerPressure = -1; // currentPressure is Sea Level Adjusted (Relative) barometer in hPa or mB
|
||||
private int pressureEvolution = -1; // pressureEvolution There are five points for registering the behavior of your
|
||||
// barometer for a period of about 6 hours prior to the forecast.
|
||||
private int nubes = -1;
|
||||
private int currentBeaufort = -1;
|
||||
private double cloudLevel = -1;
|
||||
private boolean raining = false;
|
||||
|
||||
@Activate
|
||||
public SagerWeatherCaster() {
|
||||
InputStream input = Thread.currentThread().getContextClassLoader()
|
||||
.getResourceAsStream("/sagerForecaster.properties");
|
||||
try {
|
||||
forecaster.load(input);
|
||||
} catch (IOException e) {
|
||||
logger.warn("Error during Sager Forecaster startup", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String[] getUsedDirections() {
|
||||
return usedDirections;
|
||||
}
|
||||
|
||||
public void setBearing(int newBearing, int oldBearing) {
|
||||
int windEvol = sagerWindTrend(oldBearing, newBearing);
|
||||
if ((windEvol != this.windEvolution) || (newBearing != currentBearing)) {
|
||||
this.currentBearing = newBearing;
|
||||
this.windEvolution = windEvol;
|
||||
updatePrediction();
|
||||
}
|
||||
}
|
||||
|
||||
public void setPressure(double newPressure, double oldPressure) {
|
||||
int newSagerPressure = sagerPressureLevel(newPressure);
|
||||
int pressEvol = sagerPressureTrend(newPressure, oldPressure);
|
||||
if ((pressEvol != this.pressureEvolution) || (newSagerPressure != sagerPressure)) {
|
||||
this.sagerPressure = newSagerPressure;
|
||||
this.pressureEvolution = pressEvol;
|
||||
updatePrediction();
|
||||
}
|
||||
}
|
||||
|
||||
public void setCloudLevel(int cloudiness) {
|
||||
this.cloudLevel = cloudiness;
|
||||
sagerNubesUpdate();
|
||||
}
|
||||
|
||||
public void setRaining(boolean raining) {
|
||||
this.raining = raining;
|
||||
sagerNubesUpdate();
|
||||
}
|
||||
|
||||
public void setBeaufort(int beaufortIndex) {
|
||||
if (currentBeaufort != beaufortIndex) {
|
||||
this.currentBeaufort = beaufortIndex;
|
||||
updatePrediction();
|
||||
}
|
||||
}
|
||||
|
||||
public int getBeaufort() {
|
||||
return this.currentBeaufort;
|
||||
}
|
||||
|
||||
public int getWindEvolution() {
|
||||
return this.windEvolution;
|
||||
}
|
||||
|
||||
public int getPressureEvolution() {
|
||||
return this.pressureEvolution;
|
||||
}
|
||||
|
||||
private void sagerNubesUpdate() {
|
||||
int result;
|
||||
if (!raining) {
|
||||
if (cloudLevel > 80) {
|
||||
result = 4; // overcast
|
||||
} else if (cloudLevel > 50) {
|
||||
result = 3; // mostly overcast
|
||||
} else if (cloudLevel > 20) {
|
||||
result = 2; // partly cloudy
|
||||
} else {
|
||||
result = 1; // clear
|
||||
}
|
||||
} else {
|
||||
result = 5; // raining
|
||||
}
|
||||
if (result != nubes) {
|
||||
this.nubes = result;
|
||||
updatePrediction();
|
||||
}
|
||||
}
|
||||
|
||||
private static int sagerPressureLevel(double current) {
|
||||
int result = 1;
|
||||
if (current > 1029.46) {
|
||||
result = 1;
|
||||
} else if (current > 1019.3) {
|
||||
result = 2;
|
||||
} else if (current > 1012.53) {
|
||||
result = 3;
|
||||
} else if (current > 1005.76) {
|
||||
result = 4;
|
||||
} else if (current > 999) {
|
||||
result = 5;
|
||||
} else if (current > 988.8) {
|
||||
result = 6;
|
||||
} else if (current > 975.28) {
|
||||
result = 7;
|
||||
} else {
|
||||
result = 8;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int sagerPressureTrend(double current, double historic) {
|
||||
double evol = current - historic;
|
||||
int result = 0;
|
||||
|
||||
if (evol > 1.4) {
|
||||
result = 1; // Rising Rapidly
|
||||
} else if (evol > 0.68) {
|
||||
result = 2; // Rising Slowly
|
||||
} else if (evol > -0.68) {
|
||||
result = 3; // Normal
|
||||
} else if (evol > -1.4) {
|
||||
result = 4; // Decreasing Slowly
|
||||
} else {
|
||||
result = 5; // Decreasing Rapidly
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int sagerWindTrend(double historic, double position) {
|
||||
int result = 1; // Steady
|
||||
double angle = 180 - Math.abs(Math.abs(position - historic) - 180);
|
||||
if (angle > 45) {
|
||||
int evol = (int) (historic + angle);
|
||||
evol -= (evol > 360) ? 360 : 0;
|
||||
result = (evol == position) ? 2 : 3; // Veering : Backing
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getCompass() {
|
||||
double step = 360.0 / NTZDIRECTIONS.length;
|
||||
double b = Math.floor((this.currentBearing + (step / 2.0)) / step);
|
||||
return NTZDIRECTIONS[(int) (b % NTZDIRECTIONS.length)];
|
||||
}
|
||||
|
||||
private void updatePrediction() {
|
||||
int zWind = Arrays.asList(usedDirections).indexOf(getCompass());
|
||||
String d1 = "-";
|
||||
switch (zWind) {
|
||||
case 0:
|
||||
if (windEvolution == 3) {
|
||||
d1 = "A";
|
||||
} else if (windEvolution == 1) {
|
||||
d1 = "B";
|
||||
} else if (windEvolution == 2) {
|
||||
d1 = "C";
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if (windEvolution == 3) {
|
||||
d1 = "D";
|
||||
} else if (windEvolution == 1) {
|
||||
d1 = "E";
|
||||
} else if (windEvolution == 2) {
|
||||
d1 = "F";
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (windEvolution == 3) {
|
||||
d1 = "G";
|
||||
} else if (windEvolution == 1) {
|
||||
d1 = "H";
|
||||
} else if (windEvolution == 2) {
|
||||
d1 = "J";
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
if (windEvolution == 3) {
|
||||
d1 = "K";
|
||||
} else if (windEvolution == 1) {
|
||||
d1 = "L";
|
||||
} else if (windEvolution == 2) {
|
||||
d1 = "M";
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
if (windEvolution == 3) {
|
||||
d1 = "N";
|
||||
} else if (windEvolution == 1) {
|
||||
d1 = "O";
|
||||
} else if (windEvolution == 2) {
|
||||
d1 = "P";
|
||||
}
|
||||
break;
|
||||
case 5:
|
||||
if (windEvolution == 3) {
|
||||
d1 = "Q";
|
||||
} else if (windEvolution == 1) {
|
||||
d1 = "R";
|
||||
} else if (windEvolution == 2) {
|
||||
d1 = "S";
|
||||
}
|
||||
break;
|
||||
case 6:
|
||||
if (windEvolution == 3) {
|
||||
d1 = "T";
|
||||
} else if (windEvolution == 1) {
|
||||
d1 = "U";
|
||||
} else if (windEvolution == 2) {
|
||||
d1 = "V";
|
||||
}
|
||||
break;
|
||||
case 7:
|
||||
if (windEvolution == 3) {
|
||||
d1 = "W";
|
||||
} else if (windEvolution == 1) {
|
||||
d1 = "X";
|
||||
} else if (windEvolution == 2) {
|
||||
d1 = "Y";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (currentBeaufort == 0) {
|
||||
d1 = "Z";
|
||||
}
|
||||
}
|
||||
String forecast = forecaster.getProperty(
|
||||
d1 + String.valueOf(sagerPressure) + String.valueOf(pressureEvolution) + String.valueOf(nubes));
|
||||
prevision = (forecast != null) ? Optional.of(new Prevision(forecast)) : Optional.empty();
|
||||
}
|
||||
|
||||
public String getForecast() {
|
||||
if (prevision.isPresent()) {
|
||||
char forecast = prevision.get().zForecast;
|
||||
return Character.toString(forecast);
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
public String getWindVelocity() {
|
||||
if (prevision.isPresent()) {
|
||||
char windVelocity = prevision.get().zWindVelocity;
|
||||
return Character.toString(windVelocity);
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
public String getWindDirection() {
|
||||
if (prevision.isPresent()) {
|
||||
int direction = prevision.get().zWindDirection;
|
||||
return String.valueOf(direction);
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
public String getWindDirection2() {
|
||||
if (prevision.isPresent()) {
|
||||
int direction = prevision.get().zWindDirection2;
|
||||
return String.valueOf(direction);
|
||||
} else {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
public void setLatitude(double latitude) {
|
||||
if (latitude >= 66.6) {
|
||||
usedDirections = NPZDIRECTIONS;
|
||||
} else if (latitude >= 23.5) {
|
||||
usedDirections = NTZDIRECTIONS;
|
||||
} else if (latitude >= 0) {
|
||||
usedDirections = NPZDIRECTIONS;
|
||||
} else if (latitude > -23.5) {
|
||||
usedDirections = SPZDIRECTIONS;
|
||||
} else if (latitude > -66.6) {
|
||||
usedDirections = STZDIRECTIONS;
|
||||
} else {
|
||||
usedDirections = SPZDIRECTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.sagercaster.internal;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
|
||||
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
|
||||
import org.osgi.service.component.annotations.Component;
|
||||
|
||||
/**
|
||||
* Dynamic provider of state options for WindDirections.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = { DynamicStateDescriptionProvider.class, WindDirectionStateDescriptionProvider.class })
|
||||
public class WindDirectionStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 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.sagercaster.internal.discovery;
|
||||
|
||||
import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstants.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.openhab.core.config.discovery.AbstractDiscoveryService;
|
||||
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.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 SagerCasterDiscoveryService} creates things based on the configured location.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial Contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
@Component(service = DiscoveryService.class, immediate = true, configurationPid = "discovery.sagercaster")
|
||||
public class SagerCasterDiscoveryService extends AbstractDiscoveryService {
|
||||
private final Logger logger = LoggerFactory.getLogger(SagerCasterDiscoveryService.class);
|
||||
private static final int DISCOVER_TIMEOUT_SECONDS = 30;
|
||||
private static final int LOCATION_CHANGED_CHECK_INTERVAL = 60;
|
||||
private @NonNullByDefault({}) LocationProvider locationProvider;
|
||||
private @NonNullByDefault({}) ScheduledFuture<?> sagerCasterDiscoveryJob;
|
||||
private @Nullable PointType previousLocation;
|
||||
|
||||
private static final ThingUID sagerCasterThing = new ThingUID(THING_TYPE_SAGERCASTER, LOCAL);
|
||||
|
||||
/**
|
||||
* Creates a SagerCasterDiscoveryService with enabled autostart.
|
||||
*/
|
||||
public SagerCasterDiscoveryService() {
|
||||
super(new HashSet<>(Arrays.asList(new ThingTypeUID(BINDING_ID, "-"))), DISCOVER_TIMEOUT_SECONDS, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void activate(@Nullable Map<String, @Nullable Object> configProperties) {
|
||||
super.activate(configProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void modified(@Nullable Map<String, @Nullable Object> configProperties) {
|
||||
super.modified(configProperties);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startScan() {
|
||||
logger.debug("Starting Sager Weathercaster discovery scan");
|
||||
PointType location = locationProvider.getLocation();
|
||||
if (location == null) {
|
||||
logger.debug("LocationProvider.getLocation() is not set -> Will not provide any discovery results");
|
||||
return;
|
||||
}
|
||||
createResults(location);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startBackgroundDiscovery() {
|
||||
if (sagerCasterDiscoveryJob == null) {
|
||||
sagerCasterDiscoveryJob = scheduler.scheduleWithFixedDelay(() -> {
|
||||
PointType currentLocation = locationProvider.getLocation();
|
||||
if (currentLocation != null && !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, TimeUnit.SECONDS);
|
||||
logger.debug("Scheduled SagerCaster location-changed job every {} seconds",
|
||||
LOCATION_CHANGED_CHECK_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void stopBackgroundDiscovery() {
|
||||
logger.debug("Stopping Sager Weathercaster background discovery");
|
||||
if (sagerCasterDiscoveryJob != null && !sagerCasterDiscoveryJob.isCancelled()) {
|
||||
if (sagerCasterDiscoveryJob.cancel(true)) {
|
||||
sagerCasterDiscoveryJob = null;
|
||||
logger.debug("Stopped SagerCaster device background discovery");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void createResults(PointType location) {
|
||||
String propGeolocation;
|
||||
propGeolocation = String.format("%s,%s", location.getLatitude(), location.getLongitude());
|
||||
thingDiscovered(DiscoveryResultBuilder.create(sagerCasterThing).withLabel("Local Sager Weathercaster")
|
||||
.withRepresentationProperty(CONFIG_LOCATION).withProperty(CONFIG_LOCATION, propGeolocation).build());
|
||||
}
|
||||
|
||||
@Reference
|
||||
protected void setLocationProvider(LocationProvider locationProvider) {
|
||||
this.locationProvider = locationProvider;
|
||||
}
|
||||
|
||||
protected void unsetLocationProvider(LocationProvider locationProvider) {
|
||||
this.locationProvider = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 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.sagercaster.internal.handler;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* The {@link ExpiringMap} is responsible for storing a list of values of class T
|
||||
* Values older than eldestAge are discarded at each insert of a new one
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
class ExpiringMap<T> {
|
||||
private final SortedMap<Long, T> values = new TreeMap<>();
|
||||
private Optional<T> agedValue = Optional.empty();
|
||||
private long eldestAge = 0;
|
||||
|
||||
public void setObservationPeriod(long eldestAge) {
|
||||
this.eldestAge = eldestAge;
|
||||
}
|
||||
|
||||
public void put(T newValue) {
|
||||
long now = System.currentTimeMillis();
|
||||
values.put(now, newValue);
|
||||
Optional<Long> eldestKey = values.keySet().stream().filter(key -> key < now - eldestAge).findFirst();
|
||||
if (eldestKey.isPresent()) {
|
||||
agedValue = Optional.of(values.get(eldestKey.get()));
|
||||
values.entrySet().removeIf(map -> map.getKey() <= eldestKey.get());
|
||||
}
|
||||
}
|
||||
|
||||
public Optional<T> getAgedValue() {
|
||||
return agedValue;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 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.sagercaster.internal.handler;
|
||||
|
||||
import static org.openhab.binding.sagercaster.internal.SagerCasterBindingConstants.*;
|
||||
import static org.openhab.core.library.unit.MetricPrefix.HECTO;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.measure.quantity.Angle;
|
||||
import javax.measure.quantity.Dimensionless;
|
||||
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.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
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.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.StateOption;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* The {@link SagerCasterHandler} is responsible for handling commands, which are
|
||||
* sent to one of the channels.
|
||||
*
|
||||
* @author Gaël L'hopital - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class SagerCasterHandler extends BaseThingHandler {
|
||||
private final static String FORECAST_PENDING = "0";
|
||||
private final static Set<String> SHOWERS = Collections
|
||||
.unmodifiableSet(new HashSet<>(Arrays.asList("G", "K", "L", "R", "S", "T", "U", "W")));
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(SagerCasterHandler.class);
|
||||
private final SagerWeatherCaster sagerWeatherCaster;
|
||||
private final WindDirectionStateDescriptionProvider stateDescriptionProvider;
|
||||
private int currentTemp = 0;
|
||||
|
||||
private final ExpiringMap<QuantityType<Pressure>> pressureCache = new ExpiringMap<>();
|
||||
private final ExpiringMap<QuantityType<Temperature>> temperatureCache = new ExpiringMap<>();
|
||||
private final ExpiringMap<QuantityType<Angle>> bearingCache = new ExpiringMap<>();
|
||||
|
||||
public SagerCasterHandler(Thing thing, WindDirectionStateDescriptionProvider stateDescriptionProvider,
|
||||
SagerWeatherCaster sagerWeatherCaster) {
|
||||
super(thing);
|
||||
this.stateDescriptionProvider = stateDescriptionProvider;
|
||||
this.sagerWeatherCaster = sagerWeatherCaster;
|
||||
}
|
||||
|
||||
@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.SECONDS.toMillis(observationPeriod);
|
||||
pressureCache.setObservationPeriod(period);
|
||||
bearingCache.setObservationPeriod(period);
|
||||
temperatureCache.setObservationPeriod(period);
|
||||
defineWindDirectionStateDescriptions();
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
|
||||
private void defineWindDirectionStateDescriptions() {
|
||||
List<StateOption> options = new ArrayList<>();
|
||||
String[] directions = sagerWeatherCaster.getUsedDirections();
|
||||
for (int i = 0; i < directions.length; i++) {
|
||||
int secondDirection = i < directions.length - 1 ? i + 1 : 0;
|
||||
String windDescription = directions[i] + " or " + directions[secondDirection] + " winds";
|
||||
options.add(new StateOption(String.valueOf(i + 1), windDescription));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
if (command instanceof RefreshType) {
|
||||
postNewForecast();
|
||||
} else {
|
||||
String id = channelUID.getIdWithoutGroup();
|
||||
switch (id) {
|
||||
case CHANNEL_CLOUDINESS:
|
||||
logger.debug("Octa cloud level changed, updating forecast");
|
||||
if (command instanceof QuantityType) {
|
||||
@SuppressWarnings("unchecked")
|
||||
QuantityType<Dimensionless> cloudiness = (QuantityType<Dimensionless>) command;
|
||||
scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setCloudLevel(cloudiness.intValue());
|
||||
postNewForecast();
|
||||
});
|
||||
break;
|
||||
}
|
||||
case CHANNEL_IS_RAINING:
|
||||
logger.debug("Rain status updated, updating forecast");
|
||||
if (command instanceof OnOffType) {
|
||||
OnOffType isRaining = ((OnOffType) command);
|
||||
scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setRaining(isRaining == OnOffType.ON);
|
||||
postNewForecast();
|
||||
});
|
||||
} else {
|
||||
logger.debug("Channel '{}' can only accept Switch type 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();
|
||||
});
|
||||
} else if (command instanceof DecimalType) {
|
||||
DecimalType newQtty = ((DecimalType) command);
|
||||
scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setRaining(newQtty.doubleValue() > 0);
|
||||
postNewForecast();
|
||||
});
|
||||
} else {
|
||||
logger.debug("Channel '{}' can accept Number, Number:Speed, Number:Length type commands.",
|
||||
channelUID);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_WIND_SPEED:
|
||||
logger.debug("Updated wind speed, updating forecast");
|
||||
if (command instanceof DecimalType) {
|
||||
DecimalType newValue = (DecimalType) command;
|
||||
scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setBeaufort(newValue.intValue());
|
||||
postNewForecast();
|
||||
});
|
||||
} else {
|
||||
logger.debug("Channel '{}' only accepts DecimalType commands.", channelUID);
|
||||
}
|
||||
break;
|
||||
case CHANNEL_PRESSURE:
|
||||
logger.debug("Sea-level pressure updated, updating forecast");
|
||||
if (command instanceof QuantityType) {
|
||||
@SuppressWarnings("unchecked")
|
||||
QuantityType<Pressure> newPressure = ((QuantityType<Pressure>) command)
|
||||
.toUnit(HECTO(SIUnits.PASCAL));
|
||||
if (newPressure != null) {
|
||||
pressureCache.put(newPressure);
|
||||
Optional<QuantityType<Pressure>> agedPressure = pressureCache.getAgedValue();
|
||||
if (agedPressure.isPresent()) {
|
||||
scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setPressure(newPressure.doubleValue(),
|
||||
agedPressure.get().doubleValue());
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_PRESSURETREND,
|
||||
String.valueOf(sagerWeatherCaster.getPressureEvolution()));
|
||||
postNewForecast();
|
||||
});
|
||||
} else {
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_FORECAST, FORECAST_PENDING);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_TEMPERATURE:
|
||||
logger.debug("Temperature updated");
|
||||
if (command instanceof QuantityType) {
|
||||
@SuppressWarnings("unchecked")
|
||||
QuantityType<Temperature> newTemperature = ((QuantityType<Temperature>) command)
|
||||
.toUnit(SIUnits.CELSIUS);
|
||||
if (newTemperature != null) {
|
||||
temperatureCache.put(newTemperature);
|
||||
currentTemp = newTemperature.intValue();
|
||||
Optional<QuantityType<Temperature>> agedTemperature = temperatureCache.getAgedValue();
|
||||
if (agedTemperature.isPresent()) {
|
||||
double delta = newTemperature.doubleValue() - agedTemperature.get().doubleValue();
|
||||
String trend = (delta > 3) ? "1"
|
||||
: (delta > 0.3) ? "2" : (delta > -0.3) ? "3" : (delta > -3) ? "4" : "5";
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_TEMPERATURETREND, trend);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case CHANNEL_WIND_ANGLE:
|
||||
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();
|
||||
if (agedAngle.isPresent()) {
|
||||
scheduler.submit(() -> {
|
||||
sagerWeatherCaster.setBearing(newAngle.intValue(), agedAngle.get().intValue());
|
||||
updateChannelString(GROUP_OUTPUT, CHANNEL_WINDEVOLUTION,
|
||||
String.valueOf(sagerWeatherCaster.getWindEvolution()));
|
||||
postNewForecast();
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
logger.debug("The binding can not handle command: {} on channel: {}", command, channelUID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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" : "";
|
||||
|
||||
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;
|
||||
}
|
||||
updateChannelDecimal(GROUP_OUTPUT, CHANNEL_VELOCITY_BEAUFORT, predictedBeaufort);
|
||||
}
|
||||
|
||||
protected void updateChannelString(String group, String channelId, String value) {
|
||||
ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
|
||||
if (isLinked(id)) {
|
||||
updateState(id, new StringType(value));
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateChannelDecimal(String group, String channelId, int value) {
|
||||
ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
|
||||
if (isLinked(id)) {
|
||||
updateState(id, new DecimalType(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<binding:binding id="sagercaster" 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>SagerCaster Binding</name>
|
||||
<description>The Sager Weathercaster is a scientific instrument for accurate prediction of the weather.</description>
|
||||
<author>Gaël L'hopital</author>
|
||||
|
||||
</binding:binding>
|
||||
@@ -0,0 +1,63 @@
|
||||
# binding
|
||||
binding.sagercaster.name = SagerCaster Binding
|
||||
binding.sagercaster.description = Die SagerCaster-Erweiterung wird zur Erstellung von Wettervorhersagen verwendet.
|
||||
|
||||
# channel types
|
||||
channel-type.sagercaster.forecast.state.option.0 = Warten Sie etwas länger auf eine Vorhersage
|
||||
channel-type.sagercaster.forecast.state.option.A = Gutes Wetter
|
||||
channel-type.sagercaster.forecast.state.option.B = Gutes Wetter und Erwärmung
|
||||
channel-type.sagercaster.forecast.state.option.C = Gutes Wetter und Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.D = Instabil
|
||||
channel-type.sagercaster.forecast.state.option.E = Instabil und Erwärmung
|
||||
channel-type.sagercaster.forecast.state.option.F = Instabil und Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.G = Zunehmende Bewölkung oder sehr bewölkt, gefolgt von Niederschlag oder Schauern / Schnee
|
||||
channel-type.sagercaster.forecast.state.option.G1 = Zunehmende oder sehr trübe Bewölkung, gefolgt von Niederschlag oder Schauern
|
||||
channel-type.sagercaster.forecast.state.option.G2 = Zunehmende Bewölkung oder sehr bewölkt, gefolgt von Niederschlag oder Schnee
|
||||
channel-type.sagercaster.forecast.state.option.H = Zunehmende Bewölkung oder sehr bewölkt, gefolgt von Niederschlag oder Schauern und Erwärmung
|
||||
channel-type.sagercaster.forecast.state.option.J = Regengüsse
|
||||
channel-type.sagercaster.forecast.state.option.K = Regengüsse / Schnee und Erwärmung
|
||||
channel-type.sagercaster.forecast.state.option.K1 = Regengüsse und Erwärmung
|
||||
channel-type.sagercaster.forecast.state.option.K2 = Schnee und Erwärmung
|
||||
channel-type.sagercaster.forecast.state.option.L = Regengüsse / Schnee und Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.L1 = Regengüsse und Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.L2 = Regengüsse und Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.M = Niederschlag
|
||||
channel-type.sagercaster.forecast.state.option.N = Niederschlag und Erwärmung
|
||||
channel-type.sagercaster.forecast.state.option.P = Niederschlag und Abkühlung dann wahrscheinliche Besserung innerhalb von 24 Stunden
|
||||
channel-type.sagercaster.forecast.state.option.R = Niederschlag oder Schauer / Schnee und Besserung innerhalb von 12 Stunden
|
||||
channel-type.sagercaster.forecast.state.option.R1 = Niederschlag oder Schauer und Besserung innerhalb von 12 Stunden
|
||||
channel-type.sagercaster.forecast.state.option.R2 = Niederschlag oder Schnee und Besserung innerhalb von 12 Stunden
|
||||
channel-type.sagercaster.forecast.state.option.S = Niederschlag oder Schauer / Schnee und Verbesserung innerhalb von 12 Stunden und Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.S1 = Niederschlag oder Regengüsse und Besserung innerhalb von 12 Stunden und Erfrischung
|
||||
channel-type.sagercaster.forecast.state.option.S2 = Niederschlag oder Schnee und Verbesserung innerhalb von 12 Stunden und Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.T = Niederschlag oder Schauer / Schnee und schnelle Besserung innerhalb von 6 Stunden
|
||||
channel-type.sagercaster.forecast.state.option.T1 = Niederschlag oder Schauer und schnelle Besserung innerhalb von 6 Stunden
|
||||
channel-type.sagercaster.forecast.state.option.T2 = Niederschlag oder Schnee und schnelle Besserung innerhalb von 6 Stunden
|
||||
channel-type.sagercaster.forecast.state.option.U = Niederschlag oder Schauer / Schnee und schnelle Besserung innerhalb von 6 Stunden, dann Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.U1 = Niederschlag oder Schauer und schnelle Besserung innerhalb von 6 Stunden, dann Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.U2 = Niederschlag oder Schnee und schnelle Besserung innerhalb von 6 Stunden, dann Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.W = Niederschlag oder Schauer / Schnee, gefolgt von gutem Wetter innerhalb von 6 Stunden und Erfrischung
|
||||
channel-type.sagercaster.forecast.state.option.W1 = Niederschlag oder Schauer, gefolgt von gutem Wetter innerhalb von 6 Stunden und Erfrischung
|
||||
channel-type.sagercaster.forecast.state.option.W2 = Niederschlag oder Schnee, gefolgt von gutem Wetter innerhalb von 6 Stunden und Abkühlung
|
||||
channel-type.sagercaster.forecast.state.option.X = Instabil, gefolgt von gutem Wetter
|
||||
channel-type.sagercaster.forecast.state.option.Y = Instabil, gefolgt von gutem Wetter innerhalb von 6 Stunden und Erfrischung
|
||||
|
||||
channel-type.sagercaster.velocity.state.option.N = Wahrscheinlich steigend
|
||||
channel-type.sagercaster.velocity.state.option.F = Mäßig bis frisch
|
||||
channel-type.sagercaster.velocity.state.option.S = Starke Winde können dem Sturm im offenen Raum vorausgehen
|
||||
channel-type.sagercaster.velocity.state.option.G = Sturm
|
||||
channel-type.sagercaster.velocity.state.option.W = Gefährlicher Sturm
|
||||
channel-type.sagercaster.velocity.state.option.H = Orkan
|
||||
channel-type.sagercaster.velocity.state.option.D = Abkühlend oder moderat, wenn die aktuellen Winde kühl oder stark sind
|
||||
channel-type.sagercaster.velocity.state.option.U = Keine wesentliche Änderung. Tendenz zur Zunahme während des Tages, Abnahme am Abend.
|
||||
|
||||
channel-type.sagercaster.wind-evolution.state.option.1 = Stabil
|
||||
channel-type.sagercaster.wind-evolution.state.option.2 = Stätig
|
||||
channel-type.sagercaster.wind-evolution.state.option.3 = Variabel
|
||||
|
||||
channel-type.sagercaster.trend.state.option.1 = Schneller Anstieg
|
||||
channel-type.sagercaster.trend.state.option.2 = Langsamer Anstieg
|
||||
channel-type.sagercaster.trend.state.option.3 = Stabil
|
||||
channel-type.sagercaster.trend.state.option.4 = Mäßiger Rückgang
|
||||
channel-type.sagercaster.trend.state.option.5 = Schneller Rückgang
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# binding
|
||||
binding.sagercaster.name = Extension SagerCaster
|
||||
binding.sagercaster.description = L'extension SagerCaster permet d'établir des prévisions météo.
|
||||
|
||||
# channel types
|
||||
channel-type.sagercaster.forecast.state.option.0 = Patientez encore un peu pour une prédiction
|
||||
channel-type.sagercaster.forecast.state.option.A = Beau-temps
|
||||
channel-type.sagercaster.forecast.state.option.B = Beau-temps et réchauffement
|
||||
channel-type.sagercaster.forecast.state.option.C = Beau-temps et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.D = Instable
|
||||
channel-type.sagercaster.forecast.state.option.E = Instable et réchauffement
|
||||
channel-type.sagercaster.forecast.state.option.F = Instable et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.G = Nébulosité croissante ou très nuageux suivi de précititations ou averses / neige
|
||||
channel-type.sagercaster.forecast.state.option.G1 = Nébulosité croissante ou très nuageux suivi de précititations ou averses
|
||||
channel-type.sagercaster.forecast.state.option.G2 = Nébulosité croissante ou très nuageux suivi de précititations ou neige
|
||||
channel-type.sagercaster.forecast.state.option.H = Nébulosité croissante ou très nuageux suivi de précititations ou averses et réchauffement
|
||||
channel-type.sagercaster.forecast.state.option.J = Averses
|
||||
channel-type.sagercaster.forecast.state.option.K = Averses / neige et réchauffement
|
||||
channel-type.sagercaster.forecast.state.option.K1 = Averses et réchauffement
|
||||
channel-type.sagercaster.forecast.state.option.K2 = Neige et réchauffement
|
||||
channel-type.sagercaster.forecast.state.option.L = Averses / neige et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.L1 = Averses et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.L2 = Neige et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.M = Précipitations
|
||||
channel-type.sagercaster.forecast.state.option.N = Précipitations et réchauffement
|
||||
channel-type.sagercaster.forecast.state.option.P = Précipitations et rafraichissement puis amélioration probable dans les 24 heures
|
||||
channel-type.sagercaster.forecast.state.option.R = Précipitations ou averses / neige et amélioration dans les 12 heures
|
||||
channel-type.sagercaster.forecast.state.option.R1 = Précipitations ou averses et amélioration dans les 12 heures
|
||||
channel-type.sagercaster.forecast.state.option.R2 = Précipitations ou neige et amélioration dans les 12 heures
|
||||
channel-type.sagercaster.forecast.state.option.S = Précipitations ou averses / neige et amélioration dans les 12 heures et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.S1 = Précipitations ou averses et amélioration dans les 12 heures et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.S2 = Précipitations ou neige et amélioration dans les 12 heures et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.T = Précipitations ou averses / neige et amélioration rapide dans les 6 heures
|
||||
channel-type.sagercaster.forecast.state.option.T1 = Précipitations ou averses et amélioration rapide dans les 6 heures
|
||||
channel-type.sagercaster.forecast.state.option.T2 = Précipitations ou neige et amélioration rapide dans les 6 heures
|
||||
channel-type.sagercaster.forecast.state.option.U = Précipitations ou averses / neige et amélioration rapide dans les 6 heures puis rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.U1 = Précipitations ou averses et amélioration rapide dans les 6 heures puis rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.U2 = Précipitations ou neige et amélioration rapide dans les 6 heures puis rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.W = Précipitations ou averses / neige suivi de beau temps rapide dans les 6 heures et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.W1 = Précipitations ou averses suivi de beau temps rapide dans les 6 heures et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.W2 = Précipitations ou neige suivi de beau temps rapide dans les 6 heures et rafraichissement
|
||||
channel-type.sagercaster.forecast.state.option.X = Instable suivi de beau temps
|
||||
channel-type.sagercaster.forecast.state.option.Y = Instable suivi de beau temps rapide dans les 6 heures et rafraichissement
|
||||
|
||||
channel-type.sagercaster.velocity.state.option.N = Probablement en augmentation
|
||||
channel-type.sagercaster.velocity.state.option.F = Modéré à frais
|
||||
channel-type.sagercaster.velocity.state.option.S = Vents forts pouvant précéder tempête en espace ouvert
|
||||
channel-type.sagercaster.velocity.state.option.G = Tempête
|
||||
channel-type.sagercaster.velocity.state.option.W = Tempête dangereuse
|
||||
channel-type.sagercaster.velocity.state.option.H = Ouragan
|
||||
channel-type.sagercaster.velocity.state.option.D = Décroissant ou en modération si les vents actuels sont frais ou forts
|
||||
channel-type.sagercaster.velocity.state.option.U = Pas de changement notable. Tendance pour augmentation progressive dans la journée, diminution dans la soirée.
|
||||
|
||||
channel-type.sagercaster.wind-evolution.state.option.1 = Stable
|
||||
channel-type.sagercaster.wind-evolution.state.option.2 = Horaire
|
||||
channel-type.sagercaster.wind-evolution.state.option.3 = Anti-horaire
|
||||
|
||||
channel-type.sagercaster.trend.state.option.1 = Augmentation rapide
|
||||
channel-type.sagercaster.trend.state.option.2 = Augmentation lente
|
||||
channel-type.sagercaster.trend.state.option.3 = Stable
|
||||
channel-type.sagercaster.trend.state.option.4 = Baisse modérée
|
||||
channel-type.sagercaster.trend.state.option.5 = Baisse rapide
|
||||
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<thing:thing-descriptions bindingId="sagercaster"
|
||||
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="sagercaster">
|
||||
<label>SagerCaster Thing</label>
|
||||
<description>This thing represents a forecast for a given location</description>
|
||||
|
||||
|
||||
<channel-groups>
|
||||
<channel-group id="input" typeId="input"/>
|
||||
<channel-group id="output" typeId="output"/>
|
||||
</channel-groups>
|
||||
|
||||
<representation-property>location</representation-property>
|
||||
|
||||
<config-description>
|
||||
<parameter name="location" type="text" required="true"
|
||||
pattern="^[-+]?([1-8]?\d(\.\d+)?|90(\.0+)?)[,]\s*[-+]?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$">
|
||||
<label>Location</label>
|
||||
<context>location</context>
|
||||
<description>Your geo coordinates separated with comma (e.g. "37.8,-122.4").</description>
|
||||
</parameter>
|
||||
|
||||
<parameter name="observation-period" type="integer" min="0" max="6" required="true">
|
||||
<label>Observation Period</label>
|
||||
<description>SagerWeatherCaster needs a minimum representative period of time to produce meaningfull results.
|
||||
Defaults to 6 hours</description>
|
||||
<default>6</default>
|
||||
</parameter>
|
||||
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
|
||||
<channel-group-type id="input">
|
||||
<label>Inputs</label>
|
||||
<description>The channels used to build the forecast results</description>
|
||||
<channels>
|
||||
<channel id="cloudiness" typeId="cloudiness"/>
|
||||
<channel id="is-raining" typeId="is-raining"/>
|
||||
<channel id="rain-qtty" typeId="rain-qtty"/>
|
||||
<channel id="wind-speed-beaufort" typeId="wind-speed-beaufort"/>
|
||||
<channel id="pressure" typeId="pressure"/>
|
||||
<channel id="temperature" typeId="temperature"/>
|
||||
<channel id="wind-angle" typeId="wind-angle-rw"/>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-group-type id="output">
|
||||
<label>Results</label>
|
||||
<description>Results of the Sager Weathercaster algorithm</description>
|
||||
<channels>
|
||||
<channel id="forecast" typeId="forecast"/>
|
||||
<channel id="velocity" typeId="velocity"/>
|
||||
<channel id="velocity-beaufort" typeId="wind-speed-beaufort">
|
||||
<label>Wind Velocity</label>
|
||||
</channel>
|
||||
<channel id="wind-from" typeId="wind-direction">
|
||||
<label>Wind From</label>
|
||||
</channel>
|
||||
<channel id="wind-to" typeId="wind-direction">
|
||||
<label>Wind To</label>
|
||||
</channel>
|
||||
<channel id="wind-evolution" typeId="wind-evolution"/>
|
||||
<channel id="pressure-trend" typeId="trend">
|
||||
<label>Pressure Trend</label>
|
||||
<description>Pressure Evolution trend over observation delay</description>
|
||||
</channel>
|
||||
<channel id="temperature-trend" typeId="trend">
|
||||
<label>Temperature Trend</label>
|
||||
<description>Temperature Evolution trend over observation delay</description>
|
||||
</channel>
|
||||
</channels>
|
||||
</channel-group-type>
|
||||
|
||||
<channel-type id="forecast">
|
||||
<item-type>String</item-type>
|
||||
<label>Weather Forecast</label>
|
||||
<state readOnly="true" pattern="%s">
|
||||
<options>
|
||||
<option value="0">Not enough historic data to study pressure evolution, wait a bit ...</option>
|
||||
<option value="A">Fair</option>
|
||||
<option value="B">Fair and warmer</option>
|
||||
<option value="C">Fair and cooler</option>
|
||||
<option value="D">Unsettled</option>
|
||||
<option value="E">Unsettled and warmer</option>
|
||||
<option value="F">Unsettled and cooler</option>
|
||||
<option value="G">Increasing cloudiness or overcast followed by Precipitation or showers/Flurries</option>
|
||||
<option value="G1">Increasing cloudiness or overcast followed by Precipitation or showers</option>
|
||||
<option value="G2">Increasing cloudiness or overcast followed by Precipitation or Flurries</option>
|
||||
<option value="H">Increasing cloudiness or overcast followed by Precipitation or showers and warmer</option>
|
||||
<option value="J">Showers</option>
|
||||
<option value="K">Showers/Flurries and warmer</option>
|
||||
<option value="K1">Showers and warmer</option>
|
||||
<option value="K2">Flurries and warmer</option>
|
||||
<option value="L">Showers/Flurries and cooler</option>
|
||||
<option value="L1">Showers and cooler</option>
|
||||
<option value="L2">Flurries and cooler</option>
|
||||
<option value="M">Precipitation</option>
|
||||
<option value="N">Precipitation and warmer</option>
|
||||
<option value="P">Precipitation and turning cooler; then improvement likely in 24 hours</option>
|
||||
<option value="R">Precipitation or showers/Flurries followed by improvement (within 12 hours)</option>
|
||||
<option value="R1">Precipitation or showers followed by improvement (within 12 hours)</option>
|
||||
<option value="R2">Precipitation or flurries followed by improvement (within 12 hours)</option>
|
||||
<option value="S">Precipitation or showers/Flurries followed by improvement (within 12 hours) and becoming cooler</option>
|
||||
<option value="S1">Precipitation or showers followed by improvement (within 12 hours) and becoming cooler</option>
|
||||
<option value="S2">Precipitation or flurries followed by improvement (within 12 hours) and becoming cooler</option>
|
||||
<option value="T">Precipitation or showers/Flurries followed by improvement early in period (within 6 hours)</option>
|
||||
<option value="T1">Precipitation or showers followed by improvement early in period (within 6 hours)</option>
|
||||
<option value="T2">Precipitation or flurries followed by improvement early in period (within 6 hours)</option>
|
||||
<option value="U">Precipitation or showers/Flurries by improvement early in period (within 6 hours) and becoming
|
||||
cooler</option>
|
||||
<option value="U1">Precipitation or showers by improvement early in period (within 6 hours) and becoming cooler</option>
|
||||
<option value="U2">Precipitation or flurries by improvement early in period (within 6 hours) and becoming cooler</option>
|
||||
<option value="W">Precipitation or showers/Flurries followed by fair early in period (within 6 hours) and
|
||||
becoming cooler</option>
|
||||
<option value="W1">Precipitation or showers followed by fair early in period (within 6 hours) and becoming cooler</option>
|
||||
<option value="W2">Precipitation or flurries followed by fair early in period (within 6 hours) and becoming cooler</option>
|
||||
<option value="X">Unsettled followed by fair</option>
|
||||
<option value="Y">Unsettled followed by fair early in period (within 6 hours) and becoming cooler</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="velocity">
|
||||
<item-type>String</item-type>
|
||||
<label>Wind Velocity</label>
|
||||
<state readOnly="true" pattern="%s">
|
||||
<options>
|
||||
<option value="N">Probably increasing</option>
|
||||
<option value="F">Moderate to fresh</option>
|
||||
<option value="S">Strong winds may precede gales over open water)</option>
|
||||
<option value="G">Gale</option>
|
||||
<option value="W">Dangerous gale (whole gale)</option>
|
||||
<option value="H">Hurricane</option>
|
||||
<option value="D">Diminishing, or moderating somewhat if current winds are of fresh to strong velocity</option>
|
||||
<option value="U">No important change. Some tendency for slight increase in winds during day, diminishing in
|
||||
evening</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="wind-direction">
|
||||
<item-type>String</item-type>
|
||||
<label>Wind Direction</label>
|
||||
<description>Wind direction</description>
|
||||
<category>Wind</category>
|
||||
<state readOnly="true" pattern="%s"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="wind-angle-rw">
|
||||
<item-type>Number:Angle</item-type>
|
||||
<label>Wind Angle</label>
|
||||
<description>Wind Angle</description>
|
||||
<category>Wind</category>
|
||||
<state readOnly="false" pattern="%.0f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="wind-evolution" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Wind Evolution</label>
|
||||
<description>Wind bearing evolution trend over observation delay</description>
|
||||
<state readOnly="true" pattern="%s">
|
||||
<options>
|
||||
<option value="1">Steady</option>
|
||||
<option value="2">Veering</option>
|
||||
<option value="3">Backing</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="trend" advanced="true">
|
||||
<item-type>String</item-type>
|
||||
<label>Measure Trend</label>
|
||||
<description>Measure evolution trend over observation delay</description>
|
||||
<state readOnly="true" pattern="%s">
|
||||
<options>
|
||||
<option value="1">Rising Rapidly</option>
|
||||
<option value="2">Rising Slowly</option>
|
||||
<option value="3">Normal</option>
|
||||
<option value="4">Decreasing Slowly</option>
|
||||
<option value="5">Decreasing Rapidly</option>
|
||||
</options>
|
||||
</state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="timestamp" advanced="true">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Calculation Time</label>
|
||||
<description>Weather forecast calculation date and time</description>
|
||||
<category>Observation time</category>
|
||||
<state readOnly="true"></state>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="cloudiness">
|
||||
<item-type>Number:Dimensionless</item-type>
|
||||
<label>Cloudiness</label>
|
||||
<description>Current cloudiness.</description>
|
||||
<category>Clouds</category>
|
||||
<state min="0" max="100" pattern="%d %%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="rain-qtty" advanced="true">
|
||||
<item-type>Number</item-type>
|
||||
<label>Rain Quantity</label>
|
||||
<description>Current rain quantity</description>
|
||||
<category>Rain</category>
|
||||
<state pattern="%.2f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="is-raining">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Raining</label>
|
||||
<description>Is it currently raining ?</description>
|
||||
<category>Rain</category>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="wind-speed-beaufort">
|
||||
<item-type>Number</item-type>
|
||||
<label>Beaufort</label>
|
||||
<description>Wind speed using Beaufort Scale</description>
|
||||
<category>Wind</category>
|
||||
<state min="0" max="12" pattern="%d"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="pressure">
|
||||
<item-type>Number:Pressure</item-type>
|
||||
<label>Sea Level Pressure</label>
|
||||
<description>Sea Level Pressure</description>
|
||||
<category>Pressure</category>
|
||||
<state pattern="%.3f %unit%"/>
|
||||
</channel-type>
|
||||
|
||||
<channel-type id="temperature">
|
||||
<item-type>Number:Temperature</item-type>
|
||||
<label>Temperature</label>
|
||||
<description>Current temperature</description>
|
||||
<category>Temperature</category>
|
||||
<state pattern="%.1f %unit%"/>
|
||||
</channel-type>
|
||||
</thing:thing-descriptions>
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user