2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
14 * eltiempo.selfip.com - Sager Weathercaster Algorhithm
16 * Copyright © 2008 Naish666 (eltiempo.selfip.com)
18 * Java transposition done by Gaël L'hopital - 2015
20 * This program is free software: you can redistribute it and/or modify
21 * it under the terms of the GNU General Public License as published by
22 * the Free Software Foundation, either version 3 of the License, or
23 * (at your option) any later version.
25 * This program is distributed in the hope that it will be useful,
26 * but WITHOUT ANY WARRANTY; without even the implied warranty of
27 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
28 * GNU General Public License for more details.
30 * You should have received a copy of the GNU General Public License
31 * along with this program. If not, see <http://www.gnu.org/licenses/>.
34 * BT's Global Sager Weathercaster PHP Scripts For Cumulus (Weathercaster)
35 * by "Buford T. Justice" / "BTJustice"
36 * http://www.freewebs.com/btjustice/bt-forecasters.html
39 * You may redistribute and use these PHP Scripts any way you wish as long as
40 * they remain FREE and money is not charged for their use directly or indirectly.
41 * If these PHP Scripts are used in your work or are modified in any way, please
42 * retain the full credit header.
44 * The Sager Weathercaster: A Scientific Instrument for Accurate Prediction of
46 * Copyright © 1969 by Raymond M. Sager and E. F. Sager
47 " The Sager Weathercaster predicts the weather quickly and accurately. It has been
49 * Not a novelty, not a toy, this is a highly dependable, scientifically designed
50 * tool of inestimable value to travelers, farmers, hunters, sailors, yachtsmen, campers,
51 * fishermen, students -- in fact, to everyone who needs or wants to know what
52 * the weather will be."
53 * 378 possible forecasts determined from 4996 dial codes.
56 package org.openhab.binding.sagercaster.internal;
58 import java.io.IOException;
59 import java.io.InputStream;
60 import java.util.Arrays;
61 import java.util.Optional;
62 import java.util.Properties;
64 import org.eclipse.jdt.annotation.NonNullByDefault;
65 import org.osgi.service.component.annotations.Activate;
66 import org.osgi.service.component.annotations.Component;
67 import org.osgi.service.component.annotations.ServiceScope;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
72 * This class is responsible for handling the SagerWeatherCaster algorithm
74 * @author Gaël L'hopital - Initial contribution
76 @Component(service = SagerWeatherCaster.class, scope = ServiceScope.SINGLETON)
78 public class SagerWeatherCaster {
79 private final Properties forecaster = new Properties();
81 // Northern Polar Zone & Northern Tropical Zone
82 private final static String[] NPZDIRECTIONS = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" };
83 // Northern Temperate Zone
84 private final static String[] NTZDIRECTIONS = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
85 // Southern Polar Zone & Southern Tropical Zone
86 private final static String[] SPZDIRECTIONS = { "N", "NW", "W", "SW", "S", "SE", "E", "NE" };
87 // Southern Temperate Zone
88 private final static String[] STZDIRECTIONS = { "S", "SE", "E", "NE", "N", "NW", "W", "SW" };
90 private final Logger logger = LoggerFactory.getLogger(SagerWeatherCaster.class);
92 private Optional<Prevision> prevision = Optional.empty();
93 private @NonNullByDefault({}) String[] usedDirections;
95 private int currentBearing = -1;
96 private int windEvolution = -1; // Whether the wind during the last 6 hours has changed its direction by
97 // approximately 45 degrees or more
98 private int sagerPressure = -1; // currentPressure is Sea Level Adjusted (Relative) barometer in hPa or mB
99 private int pressureEvolution = -1; // pressureEvolution There are five points for registering the behavior of your
100 // barometer for a period of about 6 hours prior to the forecast.
101 private int nubes = -1;
102 private int currentBeaufort = -1;
103 private double cloudLevel = -1;
104 private boolean raining = false;
107 public SagerWeatherCaster() {
108 InputStream input = Thread.currentThread().getContextClassLoader()
109 .getResourceAsStream("/sagerForecaster.properties");
111 forecaster.load(input);
112 } catch (IOException e) {
113 logger.warn("Error during Sager Forecaster startup", e);
117 public String[] getUsedDirections() {
118 return usedDirections;
121 public void setBearing(int newBearing, int oldBearing) {
122 int windEvol = sagerWindTrend(oldBearing, newBearing);
123 if ((windEvol != this.windEvolution) || (newBearing != currentBearing)) {
124 this.currentBearing = newBearing;
125 this.windEvolution = windEvol;
130 public void setPressure(double newPressure, double oldPressure) {
131 int newSagerPressure = sagerPressureLevel(newPressure);
132 int pressEvol = sagerPressureTrend(newPressure, oldPressure);
133 if ((pressEvol != this.pressureEvolution) || (newSagerPressure != sagerPressure)) {
134 this.sagerPressure = newSagerPressure;
135 this.pressureEvolution = pressEvol;
140 public void setCloudLevel(int cloudiness) {
141 this.cloudLevel = cloudiness;
145 public void setRaining(boolean raining) {
146 this.raining = raining;
150 public void setBeaufort(int beaufortIndex) {
151 if (currentBeaufort != beaufortIndex) {
152 this.currentBeaufort = beaufortIndex;
157 public int getBeaufort() {
158 return this.currentBeaufort;
161 public int getWindEvolution() {
162 return this.windEvolution;
165 public int getPressureEvolution() {
166 return this.pressureEvolution;
169 private void sagerNubesUpdate() {
172 if (cloudLevel > 80) {
173 result = 4; // overcast
174 } else if (cloudLevel > 50) {
175 result = 3; // mostly overcast
176 } else if (cloudLevel > 20) {
177 result = 2; // partly cloudy
182 result = 5; // raining
184 if (result != nubes) {
190 private static int sagerPressureLevel(double current) {
192 if (current > 1029.46) {
194 } else if (current > 1019.3) {
196 } else if (current > 1012.53) {
198 } else if (current > 1005.76) {
200 } else if (current > 999) {
202 } else if (current > 988.8) {
204 } else if (current > 975.28) {
212 private static int sagerPressureTrend(double current, double historic) {
213 double evol = current - historic;
217 result = 1; // Rising Rapidly
218 } else if (evol > 0.68) {
219 result = 2; // Rising Slowly
220 } else if (evol > -0.68) {
221 result = 3; // Normal
222 } else if (evol > -1.4) {
223 result = 4; // Decreasing Slowly
225 result = 5; // Decreasing Rapidly
231 private static int sagerWindTrend(double historic, double position) {
232 int result = 1; // Steady
233 double angle = 180 - Math.abs(Math.abs(position - historic) - 180);
235 int evol = (int) (historic + angle);
236 evol -= (evol > 360) ? 360 : 0;
237 result = (evol == position) ? 2 : 3; // Veering : Backing
242 private String getCompass() {
243 double step = 360.0 / NTZDIRECTIONS.length;
244 double b = Math.floor((this.currentBearing + (step / 2.0)) / step);
245 return NTZDIRECTIONS[(int) (b % NTZDIRECTIONS.length)];
248 private void updatePrediction() {
249 int zWind = Arrays.asList(usedDirections).indexOf(getCompass());
253 if (windEvolution == 3) {
255 } else if (windEvolution == 1) {
257 } else if (windEvolution == 2) {
262 if (windEvolution == 3) {
264 } else if (windEvolution == 1) {
266 } else if (windEvolution == 2) {
271 if (windEvolution == 3) {
273 } else if (windEvolution == 1) {
275 } else if (windEvolution == 2) {
280 if (windEvolution == 3) {
282 } else if (windEvolution == 1) {
284 } else if (windEvolution == 2) {
289 if (windEvolution == 3) {
291 } else if (windEvolution == 1) {
293 } else if (windEvolution == 2) {
298 if (windEvolution == 3) {
300 } else if (windEvolution == 1) {
302 } else if (windEvolution == 2) {
307 if (windEvolution == 3) {
309 } else if (windEvolution == 1) {
311 } else if (windEvolution == 2) {
316 if (windEvolution == 3) {
318 } else if (windEvolution == 1) {
320 } else if (windEvolution == 2) {
325 if (currentBeaufort == 0) {
329 String forecast = forecaster.getProperty(
330 d1 + String.valueOf(sagerPressure) + String.valueOf(pressureEvolution) + String.valueOf(nubes));
331 prevision = (forecast != null) ? Optional.of(new Prevision(forecast)) : Optional.empty();
334 public String getForecast() {
335 if (prevision.isPresent()) {
336 char forecast = prevision.get().zForecast;
337 return Character.toString(forecast);
343 public String getWindVelocity() {
344 if (prevision.isPresent()) {
345 char windVelocity = prevision.get().zWindVelocity;
346 return Character.toString(windVelocity);
352 public String getWindDirection() {
353 if (prevision.isPresent()) {
354 int direction = prevision.get().zWindDirection;
355 return String.valueOf(direction);
361 public String getWindDirection2() {
362 if (prevision.isPresent()) {
363 int direction = prevision.get().zWindDirection2;
364 return String.valueOf(direction);
370 public void setLatitude(double latitude) {
371 if (latitude >= 66.6) {
372 usedDirections = NPZDIRECTIONS;
373 } else if (latitude >= 23.5) {
374 usedDirections = NTZDIRECTIONS;
375 } else if (latitude >= 0) {
376 usedDirections = NPZDIRECTIONS;
377 } else if (latitude > -23.5) {
378 usedDirections = SPZDIRECTIONS;
379 } else if (latitude > -66.6) {
380 usedDirections = STZDIRECTIONS;
382 usedDirections = SPZDIRECTIONS;
386 private class Prevision {
387 public final char zForecast;
388 public final char zWindVelocity;
389 public final int zWindDirection;
390 public final int zWindDirection2;
392 public Prevision(String forecast) {
393 zForecast = forecast.charAt(0);
394 zWindVelocity = forecast.charAt(1);
395 zWindDirection = Character.getNumericValue(forecast.charAt(2));
396 zWindDirection2 = (forecast.length() > 3) ? Character.getNumericValue(forecast.charAt(3)) : -1;