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 // Northern Polar Zone & Northern Tropical Zone
80 private final static String[] NPZDIRECTIONS = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" };
81 // Northern Temperate Zone
82 private final static String[] NTZDIRECTIONS = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
83 // Southern Polar Zone & Southern Tropical Zone
84 private final static String[] SPZDIRECTIONS = { "N", "NW", "W", "SW", "S", "SE", "E", "NE" };
85 // Southern Temperate Zone
86 private final static String[] STZDIRECTIONS = { "S", "SE", "E", "NE", "N", "NW", "W", "SW" };
88 private final Logger logger = LoggerFactory.getLogger(SagerWeatherCaster.class);
89 private final Properties forecaster = new Properties();
91 private Optional<Prevision> prevision = Optional.empty();
92 private String[] usedDirections = NTZDIRECTIONS; // Defaulted to Northern Zone
94 private int currentBearing = -1;
95 private int windEvolution = -1; // Whether the wind during the last 6 hours has changed its direction by
96 // approximately 45 degrees or more
97 private int sagerPressure = -1; // currentPressure is Sea Level Adjusted (Relative) barometer in hPa or mB
98 private int pressureEvolution = -1; // pressureEvolution There are five points for registering the behavior of your
99 // barometer for a period of about 6 hours prior to the forecast.
100 private int nubes = -1;
101 private int currentBeaufort = -1;
102 private double cloudLevel = -1;
103 private boolean raining = false;
106 public SagerWeatherCaster() {
107 try (InputStream input = Thread.currentThread().getContextClassLoader()
108 .getResourceAsStream("/sagerForecaster.properties")) {
109 forecaster.load(input);
110 } catch (IOException e) {
111 logger.warn("Error during Sager Forecaster startup", e);
115 public String[] getUsedDirections() {
116 return usedDirections;
119 public void setBearing(int newBearing, int oldBearing) {
120 int windEvol = sagerWindTrend(oldBearing, newBearing);
121 if ((windEvol != windEvolution) || (newBearing != currentBearing)) {
122 currentBearing = newBearing;
123 windEvolution = windEvol;
128 public void setPressure(double newPressure, double oldPressure) {
129 int newSagerPressure = sagerPressureLevel(newPressure);
130 int pressEvol = sagerPressureTrend(newPressure, oldPressure);
131 if ((pressEvol != pressureEvolution) || (newSagerPressure != sagerPressure)) {
132 sagerPressure = newSagerPressure;
133 pressureEvolution = pressEvol;
138 public void setCloudLevel(int cloudiness) {
139 cloudLevel = cloudiness;
143 public void setRaining(boolean isRaining) {
148 public void setBeaufort(int beaufortIndex) {
149 if (currentBeaufort != beaufortIndex) {
150 currentBeaufort = beaufortIndex;
155 public int getBeaufort() {
156 return currentBeaufort;
159 public int getWindEvolution() {
160 return windEvolution;
163 public int getPressureEvolution() {
164 return pressureEvolution;
167 private void sagerNubesUpdate() {
170 if (cloudLevel > 80) {
171 result = 4; // overcast
172 } else if (cloudLevel > 50) {
173 result = 3; // mostly overcast
174 } else if (cloudLevel > 20) {
175 result = 2; // partly cloudy
180 result = 5; // raining
182 if (result != nubes) {
188 private static int sagerPressureLevel(double current) {
189 if (current > 1029.46) {
191 } else if (current > 1019.3) {
193 } else if (current > 1012.53) {
195 } else if (current > 1005.76) {
197 } else if (current > 999) {
199 } else if (current > 988.8) {
201 } else if (current > 975.28) {
207 private static int sagerPressureTrend(double current, double historic) {
208 double evol = current - historic;
211 return 1; // Rising Rapidly
212 } else if (evol > 0.68) {
213 return 2; // Rising Slowly
214 } else if (evol > -0.68) {
216 } else if (evol > -1.4) {
217 return 4; // Decreasing Slowly
219 return 5; // Decreasing Rapidly
222 private static int sagerWindTrend(double historic, double position) {
223 int result = 1; // Steady
224 double angle = 180 - Math.abs(Math.abs(position - historic) - 180);
226 int evol = (int) (historic + angle);
227 evol -= (evol > 360) ? 360 : 0;
228 result = (evol == position) ? 2 : 3; // Veering : Backing
233 private String getCompass() {
234 double step = 360.0 / NTZDIRECTIONS.length;
235 double b = Math.floor((currentBearing + (step / 2.0)) / step);
236 return NTZDIRECTIONS[(int) (b % NTZDIRECTIONS.length)];
239 private void updatePrediction() {
240 int zWind = Arrays.asList(usedDirections).indexOf(getCompass());
244 if (windEvolution == 3) {
246 } else if (windEvolution == 1) {
248 } else if (windEvolution == 2) {
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 (currentBeaufort == 0) {
320 String forecast = forecaster.getProperty(
321 d1 + String.valueOf(sagerPressure) + String.valueOf(pressureEvolution) + String.valueOf(nubes));
322 prevision = (forecast != null) ? Optional.of(new Prevision(forecast)) : Optional.empty();
325 public String getForecast() {
326 if (prevision.isPresent()) {
327 char forecast = prevision.get().zForecast;
328 return Character.toString(forecast);
333 public String getWindVelocity() {
334 if (prevision.isPresent()) {
335 char windVelocity = prevision.get().zWindVelocity;
336 return Character.toString(windVelocity);
341 public String getWindDirection() {
342 if (prevision.isPresent()) {
343 int direction = prevision.get().zWindDirection;
344 return String.valueOf(direction);
349 public String getWindDirection2() {
350 if (prevision.isPresent()) {
351 int direction = prevision.get().zWindDirection2;
352 return String.valueOf(direction);
357 public void setLatitude(double latitude) {
358 if (latitude >= 66.6) {
359 usedDirections = NPZDIRECTIONS;
360 } else if (latitude >= 23.5) {
361 usedDirections = NTZDIRECTIONS;
362 } else if (latitude >= 0) {
363 usedDirections = NPZDIRECTIONS;
364 } else if (latitude > -23.5) {
365 usedDirections = SPZDIRECTIONS;
366 } else if (latitude > -66.6) {
367 usedDirections = STZDIRECTIONS;
369 usedDirections = SPZDIRECTIONS;
373 private class Prevision {
374 public final char zForecast;
375 public final char zWindVelocity;
376 public final int zWindDirection;
377 public final int zWindDirection2;
379 public Prevision(String forecast) {
380 zForecast = forecast.charAt(0);
381 zWindVelocity = forecast.charAt(1);
382 zWindDirection = Character.getNumericValue(forecast.charAt(2));
383 zWindDirection2 = (forecast.length() > 3) ? Character.getNumericValue(forecast.charAt(3)) : -1;