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.synopanalyzer-${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-synopanalyzer" description="Synop Analyzer Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.synopanalyzer/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,92 @@
/**
* 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.synopanalyser.internal.discovery;
import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.THING_SYNOP;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.synopanalyser.internal.synop.StationDB;
import org.openhab.binding.synopanalyser.internal.synop.StationDB.Station;
import org.openhab.binding.synopanalyzer.internal.config.SynopAnalyzerConfiguration;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SynopAnalyzerDiscoveryService} creates things based on the configured location.
*
* @author Gaël L'hopital - Initial Contribution
*/
@NonNullByDefault
public class SynopAnalyzerDiscoveryService extends AbstractDiscoveryService {
private final Logger logger = LoggerFactory.getLogger(SynopAnalyzerDiscoveryService.class);
private static final int DISCOVER_TIMEOUT_SECONDS = 5;
private LocationProvider locationProvider;
private final StationDB stationDB;
private final Map<Integer, Double> distances = new HashMap<>();
/**
* Creates a SynopAnalyzerDiscoveryService with enabled autostart.
*
*/
public SynopAnalyzerDiscoveryService(StationDB stationDB, LocationProvider locationProvider) {
super(Collections.singleton(THING_SYNOP), DISCOVER_TIMEOUT_SECONDS);
this.locationProvider = locationProvider;
this.stationDB = stationDB;
}
@Override
public void startScan() {
logger.debug("Starting Synop Analyzer 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);
}
public void createResults(PointType serverLocation) {
distances.clear();
stationDB.stations.forEach(s -> {
PointType stationLocation = new PointType(s.getLocation());
DecimalType distance = serverLocation.distanceFrom(stationLocation);
distances.put(s.idOmm, distance.doubleValue());
});
Map<Integer, Double> result = distances.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.naturalOrder())).collect(Collectors.toMap(
Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
Integer nearestId = result.entrySet().iterator().next().getKey();
Optional<Station> station = stationDB.stations.stream().filter(s -> s.idOmm == nearestId).findFirst();
thingDiscovered(DiscoveryResultBuilder.create(new ThingUID(THING_SYNOP, Integer.toString(nearestId)))
.withLabel("Synop : " + station.get().usualName)
.withProperty(SynopAnalyzerConfiguration.STATION_ID, nearestId)
.withRepresentationProperty(SynopAnalyzerConfiguration.STATION_ID).build());
}
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.synopanalyser.internal.synop;
/**
* The {@link WindDirections} enum possible overcast descriptions
*
* @author Gaël L'hopital - Initial contribution
*/
public enum Overcast {
UNDEFINED,
CLEAR_SKY,
CLOUDY,
SKY_NOT_VISIBLE;
/**
* Returns the overcast level depending upon octa
*/
public static Overcast fromOcta(int octa) {
if (octa == 0) {
return Overcast.CLEAR_SKY;
} else if (octa > 0 && octa < 8) {
return Overcast.CLOUDY;
} else if (octa == 9) {
return Overcast.SKY_NOT_VISIBLE;
}
return Overcast.UNDEFINED;
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.synopanalyser.internal.synop;
import java.util.List;
import com.google.gson.annotations.SerializedName;
/**
* The {@link StationDB} creates is a DTO for stations.json database.
*
* @author Gaël L'hopital - Initial Contribution
*/
public class StationDB {
public class Station {
public String country;
public String pack;
@SerializedName("id_omm")
public int idOmm;
@SerializedName("numer_sta")
public long numerSta;
@SerializedName("usual_name")
public String usualName;
public double latitude;
public double longitude;
public double elevation;
@SerializedName("station_type")
public int stationType;
public String getLocation() {
return Double.toString(latitude) + "," + Double.toString(longitude);
}
}
public List<Station> stations;
}

View File

@@ -0,0 +1,317 @@
/**
* 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.synopanalyser.internal.synop;
import java.util.List;
import javax.measure.Unit;
import javax.measure.quantity.Speed;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.unit.SmartHomeUnits;
/**
* The {@link Synop} is the ancestor common class for analyzing
* Synop messages
*
* @author Jonarzz - Initial contribution
*/
@NonNullByDefault
public abstract class Synop {
protected static final int INITIAL_VALUE = -1000;
protected static final char PLUS_SIGN_TEMPERATURE = '0';
protected static final char MINUS_SIGN_TEMPERATURE = '1';
/*
* WS - WIND SPEED
*/
protected static final int WS_WILDTYPE_IN_MPS = 0;
protected static final int WS_ANEMOMETER_IN_MPS = 1;
/*
* HV - HORIZONTAL VISIBILITY [IN KILOMETERS]
* VALUES FROM "00" TO "50" AND FROM "56" TO "99"
* 00 MEANS HV = BELOW 0,1
* DECIMAL SCOPE MEANS HV = XX / 10
* UNIT SCOPE MEANS HV = XX - 50
* 89 MEANS HV = OVER 70
* 90-99 ROUGHLY NUMBERING :
* 90 - < 0,05 km
* 91 >= 0,05 < 0,2 km
* 92 >= 0,2 < 0,5 km
* 93 >= 0,5 < 1,0 km
* 94 >= 1,0 < 2,0 km
* 95 >= 2,0 < 4,0 km
* 96 >= 4,0 < 10,0 km
* 97 >= 10,0 < 20,0 km
* 98 >= 20,0 < 50,0 km
* 99 - > 50 km
* HP - high precision
*/
protected static final int HV_LESS_THAN_1_LIMIT = 10;
protected static final int HV_LESS_THAN_10_LIMIT = 60;
protected static final int HV_LESS_THAN_50_LIMIT = 84;
protected static final int HV_LESS_THAN_1_HP_LIMIT = 93;
protected static final int HV_LESS_THAN_10_HP_LIMIT = 96;
protected static final int HV_LESS_THAN_50_HP_LIMIT = 98;
public static enum HorizontalVisibility {
UNDEFINED,
LESS_THAN_1,
LESS_THAN_10,
LESS_THAN_50,
MORE_THAN_50
}
private final int VALID_STRING_LENGTH = 5;
protected final List<String> stringArray;
private int year;
private int month;
private int day;
private int hour;
private int windIndicator;
private HorizontalVisibility horizontalVisibility = HorizontalVisibility.UNDEFINED;
private float temperature;
private int octa;
private int windDirection;
private int windSpeed;
private float pressure;
protected int horizontalVisibilityInt = INITIAL_VALUE;
protected @Nullable String temperatureString;
protected @Nullable String windString;
protected @Nullable String pressureString;
public Synop(List<String> stringArray) {
this.stringArray = stringArray;
setDateHourAndWindIndicator();
setHorizontalVisibility();
setTemperature();
setWindAndOvercast();
setPressure();
}
private void setDateHourAndWindIndicator() {
String dayHourAndWindIndicator = "";
if (this instanceof SynopLand && stringArray.size() > 1) {
dayHourAndWindIndicator = stringArray.get(1);
} else if (stringArray.size() > 2) {
dayHourAndWindIndicator = stringArray.get(2);
}
if (!isValidString(dayHourAndWindIndicator)) {
return;
}
setHourOfObservation(dayHourAndWindIndicator);
setWindIndicator(dayHourAndWindIndicator);
}
private void setHourOfObservation(String str) {
try {
hour = Integer.parseInt(str.substring(2, 4));
} catch (NumberFormatException e) {
hour = INITIAL_VALUE;
}
try {
day = Integer.parseInt(str.substring(0, 2));
} catch (NumberFormatException e) {
day = INITIAL_VALUE;
}
}
private void setWindIndicator(String str) {
try {
windIndicator = Character.getNumericValue(str.charAt(4));
} catch (NumberFormatException e) {
windIndicator = INITIAL_VALUE;
}
}
private void setHorizontalVisibility() {
setHorizontalVisibilityInt();
if (horizontalVisibilityInt != INITIAL_VALUE) {
if (horizontalVisibilityInt < HV_LESS_THAN_1_LIMIT || horizontalVisibilityInt < HV_LESS_THAN_1_HP_LIMIT) {
horizontalVisibility = HorizontalVisibility.LESS_THAN_1;
} else if (horizontalVisibilityInt < HV_LESS_THAN_10_LIMIT
|| horizontalVisibilityInt < HV_LESS_THAN_10_HP_LIMIT) {
horizontalVisibility = HorizontalVisibility.LESS_THAN_10;
} else if (horizontalVisibilityInt < HV_LESS_THAN_50_LIMIT
|| horizontalVisibilityInt < HV_LESS_THAN_50_HP_LIMIT) {
horizontalVisibility = HorizontalVisibility.LESS_THAN_50;
} else {
horizontalVisibility = HorizontalVisibility.MORE_THAN_50;
}
} else {
horizontalVisibility = HorizontalVisibility.UNDEFINED;
}
}
protected abstract void setHorizontalVisibilityInt();
private void setTemperature() {
setTemperatureString();
temperature = INITIAL_VALUE;
String temperatureString = this.temperatureString;
if (temperatureString != null) {
char firstChar = temperatureString.charAt(0);
try {
float temp = Float.parseFloat(temperatureString.substring(1, 4)) / 10;
temperature = firstChar == PLUS_SIGN_TEMPERATURE ? temp
: firstChar == MINUS_SIGN_TEMPERATURE ? -temp : INITIAL_VALUE;
} catch (NumberFormatException ignore) {
}
}
}
protected abstract void setTemperatureString();
private void setWindAndOvercast() {
setWindString();
if (windString != null) {
String gustyFlag = windString.substring(0, 2);
if ("00".equals(gustyFlag)) {
setWindSpeed(true);
} else {
setOcta();
setWindDirection();
setWindSpeed(false);
}
} else {
windDirection = INITIAL_VALUE;
windSpeed = INITIAL_VALUE;
}
}
private void setOcta() {
if (windString != null) {
octa = Character.getNumericValue(windString.charAt(0));
} else {
octa = -1;
}
}
private void setWindDirection() {
if (windString != null) {
String windDirectionString = windString.substring(1, 3);
if (windDirectionString.equals("99") || windDirectionString.equals("||")) {
windDirection = INITIAL_VALUE;
} else {
try {
windDirection = Integer.parseInt(windDirectionString) * 10;
} catch (NumberFormatException e) {
windDirection = INITIAL_VALUE;
}
}
}
}
private void setWindSpeed(boolean gustyWind) {
String speedString = null;
if (windString != null) {
speedString = windString.substring(gustyWind ? 2 : 3, 5);
try {
windSpeed = Integer.parseInt(speedString);
} catch (NumberFormatException e) {
windSpeed = INITIAL_VALUE;
}
}
}
protected abstract void setWindString();
private void setPressure() {
setPressureString();
if (pressureString != null) {
String pressureTemp = pressureString.substring(1, 5);
if (pressureTemp.charAt(0) == '0') {
pressureTemp = '1' + pressureTemp;
}
try {
pressure = (float) Integer.parseInt(pressureTemp) / 10;
} catch (NumberFormatException e) {
pressure = INITIAL_VALUE;
}
}
}
protected abstract void setPressureString();
protected boolean isValidString(String str) {
return (str.length() == VALID_STRING_LENGTH);
}
public int getYear() {
return year;
}
public int getMonth() {
return month;
}
public int getDay() {
return day;
}
public int getHour() {
return hour;
}
public int getWindIndicator() {
return windIndicator;
}
public HorizontalVisibility getHorizontalVisibility() {
return horizontalVisibility;
}
public float getTemperature() {
return temperature;
}
public int getWindDirection() {
return windDirection;
}
public int getWindSpeed() {
return windSpeed;
}
public float getPressure() {
return pressure;
}
public int getOcta() {
return octa;
}
public Unit<Speed> getWindUnit() {
return (getWindIndicator() == WS_WILDTYPE_IN_MPS || getWindIndicator() == WS_ANEMOMETER_IN_MPS)
? SmartHomeUnits.METRE_PER_SECOND
: SmartHomeUnits.KNOT;
}
}

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.synopanalyser.internal.synop;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SynopLand} is responsible for analyzing Land station
* specifics Synop messages
*
* @author Jonarzz - Initial contribution
*/
@NonNullByDefault
public class SynopLand extends Synop {
private int rainfall = INITIAL_VALUE;
public SynopLand(List<String> stringArray) {
super(stringArray);
if (stringArray.size() >= 11) {
String rainfallString = stringArray.get(10);
if (isValidString(rainfallString) && rainfallString.charAt(0) == 6) {
try {
rainfall = Integer.parseInt(rainfallString.substring(1, 4));
if (rainfall >= 990) {
rainfall = 0;
}
} catch (NumberFormatException ignore) {
}
}
}
}
@Override
protected void setHorizontalVisibilityInt() {
if (stringArray.size() >= 4) {
String horizontalVisibility = stringArray.get(3);
if (isValidString(horizontalVisibility)) {
try {
horizontalVisibilityInt = Integer.parseInt(horizontalVisibility.substring(3, 5));
} catch (NumberFormatException ignore) {
}
}
}
}
@Override
protected void setTemperatureString() {
if (stringArray.size() < 6 || !isValidString(stringArray.get(5))) {
return;
}
if (stringArray.get(5).charAt(0) == '0') {
if (stringArray.size() < 7 || !isValidString(stringArray.get(6))) {
return;
}
temperatureString = stringArray.get(6).substring(1, 5);
} else if (isValidString(stringArray.get(5))) {
temperatureString = stringArray.get(5).substring(1, 5);
}
}
@Override
protected void setWindString() {
if (stringArray.size() >= 5) {
String windString = stringArray.get(4);
if (isValidString(windString)) {
this.windString = windString;
}
}
}
@Override
protected void setPressureString() {
if (stringArray.size() >= 8) {
String pressureString = stringArray.get(7);
if (isValidString(pressureString) && pressureString.charAt(0) == '3') {
this.pressureString = pressureString;
}
}
}
public int getRainfall() {
return rainfall;
}
}

View File

@@ -0,0 +1,162 @@
/**
* 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.synopanalyser.internal.synop;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SynopMobile} is responsible for analyzing Mobile station
* specifics Synop messages
*
* @author Jonarzz - Initial contribution
*/
@NonNullByDefault
public class SynopMobile extends Synop {
private float latitude;
private float longitude;
private int verticalQuadrantMultiplier;
private int horizontalQuadrantMultiplier;
public SynopMobile(List<String> stringArray) {
super(stringArray);
setLatitude();
setLongitudeAndQuadrant();
}
@Override
protected void setHorizontalVisibilityInt() {
String temp;
if (stringArray.size() < 6 || !isValidString((temp = stringArray.get(5)))) {
horizontalVisibilityInt = INITIAL_VALUE;
return;
}
try {
horizontalVisibilityInt = Integer.parseInt(temp.substring(3, 5));
} catch (NumberFormatException e) {
horizontalVisibilityInt = INITIAL_VALUE;
}
}
@Override
protected void setTemperatureString() {
String temp;
if (stringArray.size() < 8 || !isValidString((temp = stringArray.get(7)))) {
return;
}
temperatureString = temp.substring(1, 5);
}
@Override
protected void setWindString() {
String temp;
if (stringArray.size() < 7 || !isValidString((temp = stringArray.get(6)))) {
return;
}
windString = temp;
}
@Override
protected void setPressureString() {
return;
}
private void setLatitude() {
String temp;
if (stringArray.size() < 4 || !isValidString((temp = stringArray.get(3)))) {
return;
}
String latitudeString = temp.substring(2, 5);
int tempInt = 0;
try {
tempInt = Integer.parseInt(latitudeString);
} catch (NumberFormatException e) {
latitude = INITIAL_VALUE;
return;
}
latitude = (float) tempInt / 10;
}
private void setLongitudeAndQuadrant() {
String temp;
if (stringArray.size() < 5 || !isValidString((temp = stringArray.get(4)))) {
return;
}
setQuadrantMultipliers(temp.charAt(0));
setLongitude(temp.substring(1, 5));
}
private void setQuadrantMultipliers(char q) {
switch (q) {
case '1':
verticalQuadrantMultiplier = 1;
horizontalQuadrantMultiplier = 1;
break;
case '3':
verticalQuadrantMultiplier = -1;
horizontalQuadrantMultiplier = 1;
break;
case '5':
verticalQuadrantMultiplier = -1;
horizontalQuadrantMultiplier = -1;
break;
case '7':
verticalQuadrantMultiplier = 1;
horizontalQuadrantMultiplier = -1;
break;
default:
verticalQuadrantMultiplier = 0;
horizontalQuadrantMultiplier = 0;
break;
}
}
private void setLongitude(String str) {
int tempInt = 0;
try {
tempInt = Integer.parseInt(str);
} catch (NumberFormatException e) {
longitude = INITIAL_VALUE;
return;
}
longitude = (float) tempInt / 10;
}
public float getLatitude() {
return latitude;
}
public float getLongitude() {
return longitude;
}
public int getVerticalQuadrantMultiplier() {
return verticalQuadrantMultiplier;
}
public int getHorizontalQuadrantMultiplier() {
return horizontalQuadrantMultiplier;
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.synopanalyser.internal.synop;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link SynopMobile} is responsible for analyzing Mobile station
* specifics Synop messages
*
* @author Jonarzz - Initial contribution
*/
@NonNullByDefault
public class SynopShip extends SynopMobile {
public SynopShip(List<String> stringArray) {
super(stringArray);
}
@Override
protected void setPressureString() {
String temp;
if (stringArray.size() < 10 || !isValidString((temp = stringArray.get(9))) || temp.charAt(0) != '4'
|| temp.charAt(1) != '0' || temp.charAt(1) != '9') {
return;
}
pressureString = temp;
}
}

View File

@@ -0,0 +1,46 @@
/**
* 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.synopanalyser.internal.synop;
/**
* The {@link WindDirections} enum possible wind directions
*
* @author Gaël L'hopital - Initial contribution
*/
public enum WindDirections {
N,
NNE,
NE,
ENE,
E,
ESE,
SE,
SSE,
S,
SSW,
SW,
WSW,
W,
WNW,
NW,
NNW;
/**
* Returns the wind direction based on degree.
*/
public static WindDirections getWindDirection(int degree) {
double step = 360.0 / WindDirections.values().length;
double b = Math.floor((degree + (step / 2.0)) / step);
return WindDirections.values()[(int) (b % WindDirections.values().length)];
}
}

View File

@@ -0,0 +1,62 @@
/**
* 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.synopanalyzer.internal;
import static org.openhab.core.library.unit.MetricPrefix.HECTO;
import javax.measure.Unit;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Pressure;
import javax.measure.quantity.Temperature;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.library.unit.SmartHomeUnits;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link SynopAnalyzerBinding} class defines common constants, which are
* used across the whole binding.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class SynopAnalyzerBindingConstants {
public static final String BINDING_ID = "synopanalyzer";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_SYNOP = new ThingTypeUID(BINDING_ID, "synopanalyzer");
// List of all Channel ids
public static final String HORIZONTAL_VISIBILITY = "horizontal-visibility";
public static final String OCTA = "octa";
public static final String ATTENUATION_FACTOR = "attenuation-factor";
public static final String OVERCAST = "overcast";
public static final String PRESSURE = "pressure";
public static final String TEMPERATURE = "temperature";
public static final String WIND_ANGLE = "wind-angle";
public static final String WIND_DIRECTION = "wind-direction";
public static final String WIND_STRENGTH = "wind-speed";
public static final String WIND_SPEED_BEAUFORT = "wind-speed-beaufort";
public static final String TIME_UTC = "time-utc";
// Default units
public static final Unit<Temperature> TEMPERATURE_UNIT = SIUnits.CELSIUS;
public static final Unit<Pressure> PRESSURE_UNIT = HECTO(SIUnits.PASCAL);
public static final Unit<Angle> WIND_DIRECTION_UNIT = SmartHomeUnits.DEGREE_ANGLE;
// Synop message origin station codes
public static final String LAND_STATION_CODE = "AAXX";
public static final String SHIP_STATION_CODE = "BBXX";
public static final String MOBILE_LAND_STATION_CODE = "OOXX";
}

View File

@@ -0,0 +1,117 @@
/**
* 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.synopanalyzer.internal;
import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.THING_SYNOP;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.util.Collections;
import java.util.Hashtable;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.synopanalyser.internal.discovery.SynopAnalyzerDiscoveryService;
import org.openhab.binding.synopanalyser.internal.synop.StationDB;
import org.openhab.binding.synopanalyzer.internal.handler.SynopAnalyzerHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.i18n.LocationProvider;
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.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
/**
* The {@link SynopAnalyzerHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Gaël L'hopital - Initial contribution
*/
@Component(service = ThingHandlerFactory.class, configurationPid = "binding.synopanalyzer")
@NonNullByDefault
public class SynopAnalyzerHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(SynopAnalyzerHandlerFactory.class);
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_SYNOP);
private final LocationProvider locationProvider;
private final Gson gson;
private @NonNullByDefault({}) StationDB stationDB;
private @Nullable ServiceRegistration<?> serviceReg;
@Activate
public SynopAnalyzerHandlerFactory(@Reference LocationProvider locationProvider) {
this.locationProvider = locationProvider;
this.gson = new Gson();
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
return thingTypeUID.equals(THING_SYNOP) ? new SynopAnalyzerHandler(thing, locationProvider, stationDB) : null;
}
@Override
protected void activate(ComponentContext componentContext) {
super.activate(componentContext);
try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("/db/stations.json");
Reader reader = new InputStreamReader(is, "UTF-8");) {
stationDB = gson.fromJson(reader, StationDB.class);
registerDiscoveryService();
logger.debug("Discovery service for Synop Stations registered.");
} catch (IOException e) {
logger.warn("Unable to read synop stations database");
stationDB = new StationDB();
}
}
@Override
protected void deactivate(ComponentContext componentContext) {
unregisterDiscoveryService();
super.deactivate(componentContext);
}
private void registerDiscoveryService() {
SynopAnalyzerDiscoveryService discoveryService = new SynopAnalyzerDiscoveryService(stationDB, locationProvider);
serviceReg = bundleContext.registerService(DiscoveryService.class.getName(), discoveryService,
new Hashtable<>());
}
private void unregisterDiscoveryService() {
if (serviceReg != null) {
serviceReg.unregister();
serviceReg = null;
}
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.synopanalyzer.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.synopanalyzer.internal.handler.SynopAnalyzerHandler;
/**
* The {@link SynopAnalyzerConfiguration} is responsible for holding configuration
* informations needed for {@link SynopAnalyzerHandler}
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class SynopAnalyzerConfiguration {
public static final String STATION_ID = "stationId";
public long refreshInterval = 60;
public int stationId;
}

View File

@@ -0,0 +1,248 @@
/**
* 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.synopanalyzer.internal.handler;
import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.*;
import java.io.IOException;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import javax.measure.quantity.Speed;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.synopanalyser.internal.synop.Overcast;
import org.openhab.binding.synopanalyser.internal.synop.StationDB;
import org.openhab.binding.synopanalyser.internal.synop.StationDB.Station;
import org.openhab.binding.synopanalyser.internal.synop.Synop;
import org.openhab.binding.synopanalyser.internal.synop.SynopLand;
import org.openhab.binding.synopanalyser.internal.synop.SynopMobile;
import org.openhab.binding.synopanalyser.internal.synop.SynopShip;
import org.openhab.binding.synopanalyser.internal.synop.WindDirections;
import org.openhab.binding.synopanalyzer.internal.config.SynopAnalyzerConfiguration;
import org.openhab.core.i18n.LocationProvider;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.PointType;
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.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link SynopAnalyzerHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Gaël L'hopital - Initial contribution
* @author Mark Herwege - Correction for timezone treatment
*/
@NonNullByDefault
public class SynopAnalyzerHandler extends BaseThingHandler {
private static final String OGIMET_SYNOP_PATH = "http://www.ogimet.com/cgi-bin/getsynop?block=%s&begin=%s";
private static final int REQUEST_TIMEOUT_MS = 5000;
private static final DateTimeFormatter SYNOP_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHH00");
private static final double KASTEN_POWER = 3.4;
private static final double OCTA_MAX = 8.0;
private final Logger logger = LoggerFactory.getLogger(SynopAnalyzerHandler.class);
private @Nullable ScheduledFuture<?> executionJob;
// private @NonNullByDefault({}) SynopAnalyzerConfiguration configuration;
private @NonNullByDefault({}) String formattedStationId;
private final LocationProvider locationProvider;
private final StationDB stationDB;
public SynopAnalyzerHandler(Thing thing, LocationProvider locationProvider, StationDB stationDB) {
super(thing);
this.locationProvider = locationProvider;
this.stationDB = stationDB;
}
@Override
public void initialize() {
SynopAnalyzerConfiguration configuration = getConfigAs(SynopAnalyzerConfiguration.class);
formattedStationId = String.format("%05d", configuration.stationId);
logger.info("Scheduling Synop update thread to run every {} minute for Station '{}'",
configuration.refreshInterval, formattedStationId);
if (thing.getProperties().isEmpty()) {
discoverAttributes(configuration.stationId);
}
executionJob = scheduler.scheduleWithFixedDelay(this::updateSynopChannels, 0, configuration.refreshInterval,
TimeUnit.MINUTES);
updateStatus(ThingStatus.UNKNOWN);
}
protected void discoverAttributes(int stationId) {
final Map<String, String> properties = new HashMap<>();
Optional<Station> station = stationDB.stations.stream().filter(s -> stationId == s.idOmm).findFirst();
station.ifPresent(s -> {
properties.put("Usual name", s.usualName);
properties.put("Location", s.getLocation());
PointType stationLocation = new PointType(s.getLocation());
PointType serverLocation = locationProvider.getLocation();
if (serverLocation != null) {
DecimalType distance = serverLocation.distanceFrom(stationLocation);
properties.put("Distance", new QuantityType<>(distance, SIUnits.METRE).toString());
}
});
updateProperties(properties);
}
private Optional<Synop> getLastAvailableSynop() {
logger.debug("Retrieving last Synop message");
String url = forgeURL();
try {
String answer = HttpUtil.executeUrl("GET", url, REQUEST_TIMEOUT_MS);
List<String> messages = Arrays.asList(answer.split("\n"));
if (!messages.isEmpty()) {
String message = messages.get(messages.size() - 1);
logger.debug(message);
if (message.startsWith(formattedStationId)) {
logger.debug("Valid Synop message received");
List<String> messageParts = Arrays.asList(message.split(","));
String synopMessage = messageParts.get(messageParts.size() - 1);
return createSynopObject(synopMessage);
}
logger.warn("Message does not belong to station {} : {}", formattedStationId, message);
}
logger.warn("No valid Synop found for last 24h");
} catch (IOException e) {
logger.warn("Synop request timedout : {}", e.getMessage());
}
return Optional.empty();
}
private void updateSynopChannels() {
logger.debug("Updating device channels");
Optional<Synop> synop = getLastAvailableSynop();
updateStatus(synop.isPresent() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
synop.ifPresent(theSynop -> {
getThing().getChannels().forEach(channel -> {
String channelId = channel.getUID().getId();
updateState(channelId, getChannelState(channelId, theSynop));
});
});
}
private State getChannelState(String channelId, Synop synop) {
switch (channelId) {
case HORIZONTAL_VISIBILITY:
return new StringType(synop.getHorizontalVisibility().name());
case OCTA:
return new DecimalType(Math.max(0, synop.getOcta()));
case ATTENUATION_FACTOR:
double kc = Math.max(0, Math.min(synop.getOcta(), OCTA_MAX)) / OCTA_MAX;
kc = 1 - 0.75 * Math.pow(kc, KASTEN_POWER);
return new DecimalType(kc);
case OVERCAST:
int octa = synop.getOcta();
Overcast overcast = Overcast.fromOcta(octa);
return overcast == Overcast.UNDEFINED ? UnDefType.NULL : new StringType(overcast.name());
case PRESSURE:
return new QuantityType<>(synop.getPressure(), PRESSURE_UNIT);
case TEMPERATURE:
return new QuantityType<>(synop.getTemperature(), TEMPERATURE_UNIT);
case WIND_ANGLE:
return new QuantityType<>(synop.getWindDirection(), WIND_DIRECTION_UNIT);
case WIND_DIRECTION:
return new StringType(WindDirections.getWindDirection(synop.getWindDirection()).name());
case WIND_STRENGTH:
return getWindStrength(synop);
case WIND_SPEED_BEAUFORT:
QuantityType<Speed> wsKpH = getWindStrength(synop).toUnit(SIUnits.KILOMETRE_PER_HOUR);
return (wsKpH != null) ? new DecimalType(Math.round(Math.pow(wsKpH.floatValue() / 3.01, 0.666666666)))
: UnDefType.NULL;
case TIME_UTC:
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
int year = synop.getYear() == 0 ? now.getYear() : synop.getYear();
int month = synop.getMonth() == 0 ? now.getMonth().getValue() : synop.getMonth();
ZonedDateTime zdt = ZonedDateTime.of(year, month, synop.getDay(), synop.getHour(), 0, 0, 0,
ZoneOffset.UTC);
return new DateTimeType(zdt);
default:
logger.error("Unsupported channel Id '{}'", channelId);
return UnDefType.UNDEF;
}
}
/**
* Returns the wind strength depending upon the unit of the message.
*/
private QuantityType<Speed> getWindStrength(Synop synop) {
return new QuantityType<>(synop.getWindSpeed(), synop.getWindUnit());
}
private Optional<Synop> createSynopObject(String synopMessage) {
List<String> list = new ArrayList<>(Arrays.asList(synopMessage.split("\\s+")));
if (synopMessage.startsWith(LAND_STATION_CODE)) {
return Optional.of(new SynopLand(list));
} else if (synopMessage.startsWith(SHIP_STATION_CODE)) {
return Optional.of(new SynopShip(list));
} else if (synopMessage.startsWith(MOBILE_LAND_STATION_CODE)) {
return Optional.of(new SynopMobile(list));
}
return Optional.empty();
}
private String forgeURL() {
ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC).minusDays(1);
String beginDate = SYNOP_DATE_FORMAT.format(utc);
return String.format(OGIMET_SYNOP_PATH, formattedStationId, beginDate);
}
@Override
public void dispose() {
ScheduledFuture<?> job = this.executionJob;
if (job != null && !job.isCancelled()) {
job.cancel(true);
}
executionJob = null;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command == RefreshType.REFRESH) {
updateSynopChannels();
}
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="synopanalyzer" 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>Synop Analyzer Binding</name>
<description>This is the binding to download and interpret Synop messages</description>
<author>Gaël L'hopital</author>
</binding:binding>

View File

@@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="synopanalyzer"
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="synopanalyzer">
<label>Synop Message</label>
<description>The Synop Analyzer binding decodes Synop messages</description>
<channels>
<channel id="temperature" typeId="system.outdoor-temperature"/>
<channel id="pressure" typeId="system.barometric-pressure"/>
<channel id="wind-angle" typeId="system.wind-direction"/>
<channel id="wind-direction" typeId="wind-direction"/>
<channel id="wind-speed" typeId="system.wind-speed"/>
<channel id="wind-speed-beaufort" typeId="wind-speed-beaufort"/>
<channel id="overcast" typeId="overcast"/>
<channel id="octa" typeId="octa"/>
<channel id="attenuation-factor" typeId="attenuation-factor"/>
<channel id="horizontal-visibility" typeId="horizontal-visibility"/>
<channel id="time-utc" typeId="time-utc"/>
</channels>
<config-description>
<parameter name="refreshInterval" type="integer" required="true">
<label>Refresh Interval</label>
<description>The refresh interval to poll Synop messages (in minutes).</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
<parameter name="stationId" type="integer">
<label>Station Number</label>
<required>true</required>
</parameter>
</config-description>
</thing-type>
<channel-type id="wind-speed-beaufort">
<item-type>Number</item-type>
<label>Beaufort</label>
<description>Wind speed in Beaufort Scale</description>
<category>Wind</category>
<state readOnly="true" pattern="%d" min="0" max="12"/>
</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">
<options>
<option value="N">N</option>
<option value="NNE">NNE</option>
<option value="NE">NE</option>
<option value="ENE">ENE</option>
<option value="E">E</option>
<option value="ESE">ESE</option>
<option value="SE">SE</option>
<option value="SSE">SSE</option>
<option value="S">S</option>
<option value="SSW">SSW</option>
<option value="SW">SW</option>
<option value="WSW">WSW</option>
<option value="W">W</option>
<option value="WNW">WNW</option>
<option value="NW">NW</option>
<option value="NNW">NNW</option>
</options>
</state>
</channel-type>
<channel-type id="octa">
<item-type>Number</item-type>
<label>Octa</label>
<description>Octa</description>
<state readOnly="true" pattern="%d/8" min="0" max="8"/>
</channel-type>
<channel-type id="attenuation-factor">
<item-type>Number</item-type>
<label>Mitigation Factor</label>
<description>Cloud layer mitigation factor</description>
<state readOnly="true" pattern="%.1f" max="1" min="0"/>
</channel-type>
<channel-type id="overcast" advanced="true">
<item-type>String</item-type>
<label>Overcast</label>
<description>Overcast</description>
<state readOnly="true" pattern="%s">
<options>
<option value="CLEAR_SKY">Clear sky</option>
<option value="CLOUDY">Cloudy</option>
<option value="SKY_NOT_VISIBLE">Sky is not visible</option>
</options>
</state>
</channel-type>
<channel-type id="horizontal-visibility" advanced="true">
<item-type>String</item-type>
<label>Horizontal Visibility</label>
<description>Horizontal visibility range</description>
<state readOnly="true" pattern="%s">
<options>
<option value="LESS_THAN_1">Less than 1 km</option>
<option value="LESS_THAN_10">1 to 10 km</option>
<option value="LESS_THAN_50">10 to 50 km</option>
<option value="MORE_THAN_50">More than 50 km</option>
</options>
</state>
</channel-type>
<channel-type id="time-utc" advanced="true">
<item-type>DateTime</item-type>
<label>Observation Time</label>
<description>Timestamp when data was observed</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

File diff suppressed because it is too large Load Diff