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