2 * Copyright (c) 2010-2024 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.caster;
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 public static final String UNDEF = "-";
80 // Northern Polar Zone & Northern Tropical Zone
81 private static final String[] NPZDIRECTIONS = { "S", "SW", "W", "NW", "N", "NE", "E", "SE" };
82 // Northern Temperate Zone
83 private static final String[] NTZDIRECTIONS = { "N", "NE", "E", "SE", "S", "SW", "W", "NW" };
84 // Southern Polar Zone & Southern Tropical Zone
85 private static final String[] SPZDIRECTIONS = { "N", "NW", "W", "SW", "S", "SE", "E", "NE" };
86 // Southern Temperate Zone
87 private static final String[] STZDIRECTIONS = { "S", "SE", "E", "NE", "N", "NW", "W", "SW" };
89 private final Logger logger = LoggerFactory.getLogger(SagerWeatherCaster.class);
90 private final Properties forecaster = new Properties();
92 private Optional<SagerPrediction> prevision = Optional.empty();
93 private String[] usedDirections = NTZDIRECTIONS; // Defaulted to Northern Zone
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 try (InputStream input = Thread.currentThread().getContextClassLoader()
109 .getResourceAsStream("/sagerForecaster.properties")) {
110 forecaster.load(input);
111 } catch (IOException e) {
112 logger.warn("Error during Sager Forecaster startup", e);
116 public String[] getUsedDirections() {
117 return usedDirections;
120 public void setBearing(int newBearing, int oldBearing) {
121 int windEvol = sagerWindTrend(oldBearing, newBearing);
122 if ((windEvol != windEvolution) || (newBearing != currentBearing)) {
123 currentBearing = newBearing;
124 windEvolution = windEvol;
129 public void setPressure(double newPressure, double oldPressure) {
130 int newSagerPressure = sagerPressureLevel(newPressure);
131 int pressEvol = sagerPressureTrend(newPressure, oldPressure);
132 if ((pressEvol != pressureEvolution) || (newSagerPressure != sagerPressure)) {
133 sagerPressure = newSagerPressure;
134 pressureEvolution = pressEvol;
139 public void setCloudLevel(int cloudiness) {
140 cloudLevel = cloudiness;
144 public void setRaining(boolean isRaining) {
149 public void setBeaufort(int beaufortIndex) {
150 if (currentBeaufort != beaufortIndex) {
151 currentBeaufort = beaufortIndex;
156 public int getBeaufort() {
157 return currentBeaufort;
160 public int getWindEvolution() {
161 return windEvolution;
164 public int getPressureEvolution() {
165 return pressureEvolution;
168 private void sagerNubesUpdate() {
171 if (cloudLevel > 80) {
172 result = 4; // overcast
173 } else if (cloudLevel > 50) {
174 result = 3; // mostly overcast
175 } else if (cloudLevel > 20) {
176 result = 2; // partly cloudy
181 result = 5; // raining
183 if (result != nubes) {
189 private static int sagerPressureLevel(double current) {
190 if (current > 1029.46) {
192 } else if (current > 1019.3) {
194 } else if (current > 1012.53) {
196 } else if (current > 1005.76) {
198 } else if (current > 999) {
200 } else if (current > 988.8) {
202 } else if (current > 975.28) {
208 private static int sagerPressureTrend(double current, double historic) {
209 double evol = current - historic;
212 return 1; // Rising Rapidly
213 } else if (evol > 0.68) {
214 return 2; // Rising Slowly
215 } else if (evol > -0.68) {
217 } else if (evol > -1.4) {
218 return 4; // Decreasing Slowly
220 return 5; // Decreasing Rapidly
223 private static int sagerWindTrend(double historic, double position) {
224 int result = 1; // Steady
225 double angle = 180 - Math.abs(Math.abs(position - historic) - 180);
227 int evol = (int) (historic + angle);
228 evol -= (evol > 360) ? 360 : 0;
229 result = (evol == position) ? 2 : 3; // Veering : Backing
234 private String getCompass() {
235 double step = 360.0 / NTZDIRECTIONS.length;
236 double b = Math.floor((currentBearing + (step / 2.0)) / step);
237 return NTZDIRECTIONS[(int) (b % NTZDIRECTIONS.length)];
240 private void updatePrediction() {
241 int zWind = Arrays.asList(usedDirections).indexOf(getCompass());
245 if (windEvolution == 3) {
247 } else if (windEvolution == 1) {
249 } else if (windEvolution == 2) {
254 if (windEvolution == 3) {
256 } else if (windEvolution == 1) {
258 } else if (windEvolution == 2) {
263 if (windEvolution == 3) {
265 } else if (windEvolution == 1) {
267 } else if (windEvolution == 2) {
272 if (windEvolution == 3) {
274 } else if (windEvolution == 1) {
276 } else if (windEvolution == 2) {
281 if (windEvolution == 3) {
283 } else if (windEvolution == 1) {
285 } else if (windEvolution == 2) {
290 if (windEvolution == 3) {
292 } else if (windEvolution == 1) {
294 } else if (windEvolution == 2) {
299 if (windEvolution == 3) {
301 } else if (windEvolution == 1) {
303 } else if (windEvolution == 2) {
308 if (windEvolution == 3) {
310 } else if (windEvolution == 1) {
312 } else if (windEvolution == 2) {
317 if (currentBeaufort == 0) {
321 String forecast = forecaster.getProperty(d1 + sagerPressure + pressureEvolution + nubes);
322 prevision = Optional.ofNullable(forecast != null ? new SagerPrediction(forecast) : null);
325 public String getForecast() {
326 return prevision.map(p -> p.getForecast()).orElse(UNDEF);
329 public String getWindVelocity() {
330 return prevision.map(p -> p.getWindVelocity()).orElse(UNDEF);
333 public String getWindDirection() {
334 return prevision.map(p -> p.getWindDirection()).orElse(UNDEF);
337 public String getWindDirection2() {
338 return prevision.map(p -> p.getWindDirection2()).orElse(UNDEF);
341 public String getSagerCode() {
342 return prevision.map(p -> p.getSagerCode()).orElse(UNDEF);
345 public void setLatitude(double latitude) {
346 if (latitude >= 66.6) {
347 usedDirections = NPZDIRECTIONS;
348 } else if (latitude >= 23.5) {
349 usedDirections = NTZDIRECTIONS;
350 } else if (latitude >= 0) {
351 usedDirections = NPZDIRECTIONS;
352 } else if (latitude > -23.5) {
353 usedDirections = SPZDIRECTIONS;
354 } else if (latitude > -66.6) {
355 usedDirections = STZDIRECTIONS;
357 usedDirections = SPZDIRECTIONS;
361 public int getPredictedBeaufort() {
362 int result = currentBeaufort;
363 switch (getWindVelocity()) {