]> git.basschouten.com Git - openhab-addons.git/blob
e0974655d2c2e4031f5d81c5a3b8aacb7f1032c1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.synopanalyzer.internal.handler;
14
15 import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.*;
16
17 import java.io.IOException;
18 import java.time.ZoneOffset;
19 import java.time.ZonedDateTime;
20 import java.time.format.DateTimeFormatter;
21 import java.util.ArrayList;
22 import java.util.Arrays;
23 import java.util.HashMap;
24 import java.util.List;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
29
30 import javax.measure.quantity.Speed;
31 import javax.ws.rs.HttpMethod;
32
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.synopanalyzer.internal.config.SynopAnalyzerConfiguration;
36 import org.openhab.binding.synopanalyzer.internal.synop.Overcast;
37 import org.openhab.binding.synopanalyzer.internal.synop.StationDB;
38 import org.openhab.binding.synopanalyzer.internal.synop.Synop;
39 import org.openhab.binding.synopanalyzer.internal.synop.SynopLand;
40 import org.openhab.binding.synopanalyzer.internal.synop.SynopMobile;
41 import org.openhab.binding.synopanalyzer.internal.synop.SynopShip;
42 import org.openhab.binding.synopanalyzer.internal.synop.WindDirections;
43 import org.openhab.core.i18n.LocationProvider;
44 import org.openhab.core.io.net.http.HttpUtil;
45 import org.openhab.core.library.types.DateTimeType;
46 import org.openhab.core.library.types.DecimalType;
47 import org.openhab.core.library.types.PointType;
48 import org.openhab.core.library.types.QuantityType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.library.unit.SIUnits;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.binding.BaseThingHandler;
55 import org.openhab.core.types.Command;
56 import org.openhab.core.types.RefreshType;
57 import org.openhab.core.types.State;
58 import org.openhab.core.types.UnDefType;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 /**
63  * The {@link SynopAnalyzerHandler} is responsible for handling commands, which are
64  * sent to one of the channels.
65  *
66  * @author GaĆ«l L'hopital - Initial contribution
67  * @author Mark Herwege - Correction for timezone treatment
68  */
69 @NonNullByDefault
70 public class SynopAnalyzerHandler extends BaseThingHandler {
71     private static final String OGIMET_SYNOP_PATH = "http://www.ogimet.com/cgi-bin/getsynop?block=%s&begin=%s";
72     private static final int REQUEST_TIMEOUT_MS = 5000;
73     private static final DateTimeFormatter SYNOP_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHH00");
74     private static final double KASTEN_POWER = 3.4;
75     private static final double OCTA_MAX = 8.0;
76
77     private final Logger logger = LoggerFactory.getLogger(SynopAnalyzerHandler.class);
78
79     private @Nullable ScheduledFuture<?> executionJob;
80     private @NonNullByDefault({}) String formattedStationId;
81     private final LocationProvider locationProvider;
82     private final @Nullable StationDB stationDB;
83
84     public SynopAnalyzerHandler(Thing thing, LocationProvider locationProvider, @Nullable StationDB stationDB) {
85         super(thing);
86         this.locationProvider = locationProvider;
87         this.stationDB = stationDB;
88     }
89
90     @Override
91     public void initialize() {
92         SynopAnalyzerConfiguration configuration = getConfigAs(SynopAnalyzerConfiguration.class);
93         formattedStationId = String.format("%05d", configuration.stationId);
94         logger.info("Scheduling Synop update thread to run every {} minute for Station '{}'",
95                 configuration.refreshInterval, formattedStationId);
96
97         StationDB stations = stationDB;
98         if (thing.getProperties().isEmpty() && stations != null) {
99             discoverAttributes(stations, configuration.stationId);
100         }
101
102         updateStatus(ThingStatus.UNKNOWN);
103
104         executionJob = scheduler.scheduleWithFixedDelay(this::updateSynopChannels, 0, configuration.refreshInterval,
105                 TimeUnit.MINUTES);
106     }
107
108     protected void discoverAttributes(StationDB stations, int stationId) {
109         stations.stations.stream().filter(s -> stationId == s.idOmm).findFirst().ifPresent(s -> {
110             Map<String, String> properties = new HashMap<>();
111             properties.put("Usual name", s.usualName);
112             properties.put("Location", s.getLocation());
113
114             PointType stationLocation = new PointType(s.getLocation());
115             PointType serverLocation = locationProvider.getLocation();
116             if (serverLocation != null) {
117                 DecimalType distance = serverLocation.distanceFrom(stationLocation);
118
119                 properties.put("Distance", new QuantityType<>(distance, SIUnits.METRE).toString());
120             }
121             updateProperties(properties);
122         });
123     }
124
125     private Optional<Synop> getLastAvailableSynop() {
126         logger.debug("Retrieving last Synop message");
127
128         String url = forgeURL();
129         try {
130             String answer = HttpUtil.executeUrl(HttpMethod.GET, url, REQUEST_TIMEOUT_MS);
131             List<String> messages = Arrays.asList(answer.split("\n"));
132             if (!messages.isEmpty()) {
133                 String message = messages.get(messages.size() - 1);
134                 logger.debug(message);
135                 if (message.startsWith(formattedStationId)) {
136                     logger.debug("Valid Synop message received");
137
138                     List<String> messageParts = Arrays.asList(message.split(","));
139                     String synopMessage = messageParts.get(messageParts.size() - 1);
140
141                     return createSynopObject(synopMessage);
142                 }
143                 logger.warn("Message does not belong to station {} : {}", formattedStationId, message);
144             }
145             logger.warn("No valid Synop found for last 24h");
146         } catch (IOException e) {
147             logger.warn("Synop request timedout : {}", e.getMessage());
148         }
149         return Optional.empty();
150     }
151
152     private void updateSynopChannels() {
153         logger.debug("Updating device channels");
154
155         Optional<Synop> synop = getLastAvailableSynop();
156         updateStatus(synop.isPresent() ? ThingStatus.ONLINE : ThingStatus.OFFLINE);
157         synop.ifPresent(theSynop -> {
158             getThing().getChannels().forEach(channel -> {
159                 String channelId = channel.getUID().getId();
160                 if (isLinked(channelId)) {
161                     updateState(channelId, getChannelState(channelId, theSynop));
162                 }
163             });
164         });
165     }
166
167     private State getChannelState(String channelId, Synop synop) {
168         switch (channelId) {
169             case HORIZONTAL_VISIBILITY:
170                 return new StringType(synop.getHorizontalVisibility().name());
171             case OCTA:
172                 return new DecimalType(Math.max(0, synop.getOcta()));
173             case ATTENUATION_FACTOR:
174                 double kc = Math.max(0, Math.min(synop.getOcta(), OCTA_MAX)) / OCTA_MAX;
175                 kc = 1 - 0.75 * Math.pow(kc, KASTEN_POWER);
176                 return new DecimalType(kc);
177             case OVERCAST:
178                 int octa = synop.getOcta();
179                 Overcast overcast = Overcast.fromOcta(octa);
180                 return overcast == Overcast.UNDEFINED ? UnDefType.NULL : new StringType(overcast.name());
181             case PRESSURE:
182                 return new QuantityType<>(synop.getPressure(), PRESSURE_UNIT);
183             case TEMPERATURE:
184                 return new QuantityType<>(synop.getTemperature(), TEMPERATURE_UNIT);
185             case WIND_ANGLE:
186                 return new QuantityType<>(synop.getWindDirection(), WIND_DIRECTION_UNIT);
187             case WIND_DIRECTION:
188                 return new StringType(WindDirections.getWindDirection(synop.getWindDirection()).name());
189             case WIND_STRENGTH:
190                 return getWindStrength(synop);
191             case WIND_SPEED_BEAUFORT:
192                 QuantityType<Speed> wsKpH = getWindStrength(synop).toUnit(SIUnits.KILOMETRE_PER_HOUR);
193                 return (wsKpH != null) ? new DecimalType(Math.round(Math.pow(wsKpH.floatValue() / 3.01, 0.666666666)))
194                         : UnDefType.NULL;
195             case TIME_UTC:
196                 ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
197                 int year = synop.getYear() == 0 ? now.getYear() : synop.getYear();
198                 int month = synop.getMonth() == 0 ? now.getMonth().getValue() : synop.getMonth();
199                 ZonedDateTime zdt = ZonedDateTime.of(year, month, synop.getDay(), synop.getHour(), 0, 0, 0,
200                         ZoneOffset.UTC);
201                 return new DateTimeType(zdt);
202             default:
203                 logger.error("Unsupported channel Id '{}'", channelId);
204                 return UnDefType.UNDEF;
205         }
206     }
207
208     /**
209      * Returns the wind strength depending upon the unit of the message.
210      */
211     private QuantityType<Speed> getWindStrength(Synop synop) {
212         return new QuantityType<>(synop.getWindSpeed(), synop.getWindUnit());
213     }
214
215     private Optional<Synop> createSynopObject(String synopMessage) {
216         List<String> list = new ArrayList<>(Arrays.asList(synopMessage.split("\\s+")));
217         if (synopMessage.startsWith(LAND_STATION_CODE)) {
218             return Optional.of(new SynopLand(list));
219         } else if (synopMessage.startsWith(SHIP_STATION_CODE)) {
220             return Optional.of(new SynopShip(list));
221         } else if (synopMessage.startsWith(MOBILE_LAND_STATION_CODE)) {
222             return Optional.of(new SynopMobile(list));
223         }
224         return Optional.empty();
225     }
226
227     private String forgeURL() {
228         ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC).minusDays(1);
229         String beginDate = SYNOP_DATE_FORMAT.format(utc);
230         return String.format(OGIMET_SYNOP_PATH, formattedStationId, beginDate);
231     }
232
233     @Override
234     public void dispose() {
235         ScheduledFuture<?> job = this.executionJob;
236         if (job != null && !job.isCancelled()) {
237             job.cancel(true);
238         }
239         executionJob = null;
240     }
241
242     @Override
243     public void handleCommand(ChannelUID channelUID, Command command) {
244         if (command == RefreshType.REFRESH) {
245             updateSynopChannels();
246         }
247     }
248 }