2 * Copyright (c) 2010-2022 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
13 package org.openhab.binding.synopanalyzer.internal.handler;
15 import static org.openhab.binding.synopanalyzer.internal.SynopAnalyzerBindingConstants.*;
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;
26 import java.util.Optional;
27 import java.util.concurrent.ScheduledFuture;
28 import java.util.concurrent.TimeUnit;
30 import javax.measure.quantity.Speed;
31 import javax.ws.rs.HttpMethod;
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;
64 * The {@link SynopAnalyzerHandler} is responsible for handling commands, which are
65 * sent to one of the channels.
67 * @author Gaƫl L'hopital - Initial contribution
68 * @author Mark Herwege - Correction for timezone treatment
71 public class SynopAnalyzerHandler extends BaseThingHandler {
72 private static final String OGIMET_SYNOP_PATH = "http://www.ogimet.com/cgi-bin/getsynop?block=%s&begin=%s";
73 private static final int REQUEST_TIMEOUT_MS = 5000;
74 private static final DateTimeFormatter SYNOP_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMddHH00");
75 private static final double KASTEN_POWER = 3.4;
76 private static final double OCTA_MAX = 8.0;
78 private final Logger logger = LoggerFactory.getLogger(SynopAnalyzerHandler.class);
79 private final LocationProvider locationProvider;
80 private final List<Station> stations;
82 private Optional<ScheduledFuture<?>> refreshJob = Optional.empty();
83 private @NonNullByDefault({}) String formattedStationId;
85 public SynopAnalyzerHandler(Thing thing, LocationProvider locationProvider, List<Station> stations) {
87 this.locationProvider = locationProvider;
88 this.stations = stations;
92 public void initialize() {
93 SynopAnalyzerConfiguration configuration = getConfigAs(SynopAnalyzerConfiguration.class);
94 formattedStationId = String.format("%05d", configuration.stationId);
95 logger.info("Scheduling Synop update thread to run every {} minute for Station '{}'",
96 configuration.refreshInterval, formattedStationId);
98 if (thing.getProperties().isEmpty()) {
99 discoverAttributes(configuration.stationId, locationProvider.getLocation());
102 updateStatus(ThingStatus.UNKNOWN);
104 refreshJob = Optional.of(scheduler.scheduleWithFixedDelay(this::updateChannels, 0,
105 configuration.refreshInterval, TimeUnit.MINUTES));
108 private void discoverAttributes(int stationId, @Nullable PointType serverLocation) {
109 stations.stream().filter(s -> stationId == s.idOmm).findFirst().ifPresent(station -> {
110 Map<String, String> properties = new HashMap<>(
111 Map.of("Usual name", station.usualName, "Location", station.getLocation()));
113 if (serverLocation != null) {
114 PointType stationLocation = new PointType(station.getLocation());
115 DecimalType distance = serverLocation.distanceFrom(stationLocation);
117 properties.put("Distance", new QuantityType<>(distance, SIUnits.METRE).toString());
119 updateProperties(properties);
123 private Optional<Synop> getLastAvailableSynop() {
124 logger.debug("Retrieving last Synop message");
126 String url = forgeURL();
128 String answer = HttpUtil.executeUrl(HttpMethod.GET, url, REQUEST_TIMEOUT_MS);
129 List<String> messages = Arrays.asList(answer.split("\n"));
130 if (!messages.isEmpty()) {
131 String message = messages.get(messages.size() - 1);
132 logger.debug(message);
133 if (message.startsWith(formattedStationId)) {
134 logger.debug("Valid Synop message received");
136 List<String> messageParts = Arrays.asList(message.split(","));
137 String synopMessage = messageParts.get(messageParts.size() - 1);
139 return createSynopObject(synopMessage);
141 logger.warn("Message does not belong to station {} : {}", formattedStationId, message);
143 logger.warn("No valid Synop found for last 24h");
144 } catch (IOException e) {
145 logger.warn("Synop request timedout : {}", e.getMessage());
147 return Optional.empty();
150 private void updateChannels() {
151 logger.debug("Updating device channels");
153 getLastAvailableSynop().ifPresentOrElse(synop -> {
154 updateStatus(ThingStatus.ONLINE);
155 getThing().getChannels().forEach(channel -> {
156 String channelId = channel.getUID().getId();
157 if (isLinked(channelId)) {
158 updateState(channelId, getChannelState(channelId, synop));
161 }, () -> updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE, "No Synop message available"));
164 private State getChannelState(String channelId, Synop synop) {
165 int octa = synop.getOcta();
167 case HORIZONTAL_VISIBILITY:
168 return new StringType(synop.getHorizontalVisibility().name());
170 return octa >= 0 ? new DecimalType(octa) : UnDefType.NULL;
171 case ATTENUATION_FACTOR:
173 double kc = Math.max(0, Math.min(octa, OCTA_MAX)) / OCTA_MAX;
174 kc = 1 - 0.75 * Math.pow(kc, KASTEN_POWER);
175 return new DecimalType(kc);
177 return UnDefType.NULL;
179 Overcast overcast = Overcast.fromOcta(octa);
180 return overcast == Overcast.UNDEFINED ? UnDefType.NULL : new StringType(overcast.name());
182 return new QuantityType<>(synop.getPressure(), PRESSURE_UNIT);
184 return new QuantityType<>(synop.getTemperature(), TEMPERATURE_UNIT);
186 return new QuantityType<>(synop.getWindDirection(), WIND_DIRECTION_UNIT);
188 return new StringType(WindDirections.getWindDirection(synop.getWindDirection()).name());
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)))
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 return new DateTimeType(
200 ZonedDateTime.of(year, month, synop.getDay(), synop.getHour(), 0, 0, 0, ZoneOffset.UTC));
202 logger.error("Unsupported channel Id '{}'", channelId);
203 return UnDefType.UNDEF;
208 * Returns the wind strength depending upon the unit of the message.
210 private QuantityType<Speed> getWindStrength(Synop synop) {
211 return new QuantityType<>(synop.getWindSpeed(), synop.getWindUnit());
214 private Optional<Synop> createSynopObject(String synopMessage) {
215 List<String> list = new ArrayList<>(Arrays.asList(synopMessage.split("\\s+")));
216 if (synopMessage.startsWith(LAND_STATION_CODE)) {
217 return Optional.of(new SynopLand(list));
218 } else if (synopMessage.startsWith(SHIP_STATION_CODE)) {
219 return Optional.of(new SynopShip(list));
220 } else if (synopMessage.startsWith(MOBILE_LAND_STATION_CODE)) {
221 return Optional.of(new SynopMobile(list));
223 return Optional.empty();
226 private String forgeURL() {
227 ZonedDateTime utc = ZonedDateTime.now(ZoneOffset.UTC).minusDays(1);
228 String beginDate = SYNOP_DATE_FORMAT.format(utc);
229 return String.format(OGIMET_SYNOP_PATH, formattedStationId, beginDate);
233 public void dispose() {
234 refreshJob.ifPresent(job -> job.cancel(true));
235 refreshJob = Optional.empty();
239 public void handleCommand(ChannelUID channelUID, Command command) {
240 if (command == RefreshType.REFRESH) {