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
13 package org.openhab.binding.vigicrues.internal.handler;
15 import static org.openhab.binding.vigicrues.internal.VigiCruesBindingConstants.*;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.time.ZonedDateTime;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.List;
23 import java.util.Locale;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.stream.Collectors;
29 import javax.measure.Unit;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.openhab.binding.vigicrues.internal.StationConfiguration;
34 import org.openhab.binding.vigicrues.internal.api.ApiHandler;
35 import org.openhab.binding.vigicrues.internal.api.VigiCruesException;
36 import org.openhab.binding.vigicrues.internal.dto.hubeau.HubEauResponse;
37 import org.openhab.binding.vigicrues.internal.dto.hubeau.HubEauResponse.StationData;
38 import org.openhab.binding.vigicrues.internal.dto.opendatasoft.OpenDatasoftResponse;
39 import org.openhab.binding.vigicrues.internal.dto.vigicrues.CdStationHydro;
40 import org.openhab.binding.vigicrues.internal.dto.vigicrues.InfoVigiCru;
41 import org.openhab.binding.vigicrues.internal.dto.vigicrues.TerEntVigiCru;
42 import org.openhab.binding.vigicrues.internal.dto.vigicrues.TronEntVigiCru;
43 import org.openhab.binding.vigicrues.internal.dto.vigicrues.VicANMoinsUn;
44 import org.openhab.core.i18n.LocationProvider;
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.RawType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.library.unit.SIUnits;
52 import org.openhab.core.library.unit.Units;
53 import org.openhab.core.thing.Channel;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.thing.binding.BaseThingHandler;
59 import org.openhab.core.thing.binding.builder.ThingBuilder;
60 import org.openhab.core.types.Command;
61 import org.openhab.core.types.RefreshType;
62 import org.openhab.core.types.UnDefType;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
67 * The {@link VigiCruesHandler} is responsible for querying the API and
70 * @author Gaƫl L'hopital - Initial contribution
73 public class VigiCruesHandler extends BaseThingHandler {
74 private final Logger logger = LoggerFactory.getLogger(VigiCruesHandler.class);
75 private final LocationProvider locationProvider;
77 private @Nullable ScheduledFuture<?> refreshJob;
78 private final ApiHandler apiHandler;
80 private List<QuantityType<?>> referenceHeights = new ArrayList<>();
81 private List<QuantityType<?>> referenceFlows = new ArrayList<>();
82 private @Nullable String portion;
84 public VigiCruesHandler(Thing thing, LocationProvider locationProvider, ApiHandler apiHandler) {
86 this.apiHandler = apiHandler;
87 this.locationProvider = locationProvider;
91 public void initialize() {
92 logger.debug("Initializing VigiCrues handler.");
94 StationConfiguration config = getConfigAs(StationConfiguration.class);
95 logger.debug("config refresh = {} min", config.refresh);
97 updateStatus(ThingStatus.UNKNOWN);
99 if (thing.getProperties().isEmpty()) {
100 Map<String, String> properties = discoverAttributes(config);
101 updateProperties(properties);
104 refreshJob = scheduler.scheduleWithFixedDelay(this::updateAndPublish, 0, config.refresh, TimeUnit.MINUTES);
107 private void getReferences() {
108 List<QuantityType<?>> heights = new ArrayList<>();
109 List<QuantityType<?>> flows = new ArrayList<>();
110 thing.getProperties().keySet().stream().filter(key -> key.startsWith(FLOOD)).forEach(key -> {
111 String value = thing.getProperties().get(key);
113 if (key.contains(FLOW)) {
114 flows.add(new QuantityType<>(value));
116 heights.add(new QuantityType<>(value));
120 referenceHeights = heights.stream().distinct().sorted().collect(Collectors.toList());
121 referenceFlows = flows.stream().distinct().sorted().collect(Collectors.toList());
122 portion = thing.getProperties().get(TRONCON);
125 private Map<String, String> discoverAttributes(StationConfiguration config) {
126 Map<String, String> properties = new HashMap<>();
128 ThingBuilder thingBuilder = editThing();
129 List<Channel> channels = new ArrayList<>(getThing().getChannels());
132 HubEauResponse stationDetails = apiHandler.discoverStations(config.id);
133 List<StationData> stations = stationDetails.stations;
134 if (stations != null && stations.size() > 0) {
135 stationDetails.stations.stream().findFirst().ifPresent(station -> {
136 PointType stationLocation = new PointType(
137 String.format(Locale.US, "%f,%f", station.latitudeStation, station.longitudeStation));
138 properties.put(LOCATION, stationLocation.toString());
139 PointType serverLocation = locationProvider.getLocation();
140 if (serverLocation != null) {
141 DecimalType distance = serverLocation.distanceFrom(stationLocation);
142 properties.put(DISTANCE, new QuantityType<>(distance, SIUnits.METRE).toString());
144 properties.put(RIVER, station.libelleCoursEau);
147 throw new VigiCruesException("No stations provided");
149 } catch (VigiCruesException e) {
150 logger.info("Unable to retrieve station location details {} : {}", config.id, e.getMessage());
154 CdStationHydro refineStation = apiHandler.getStationDetails(config.id);
155 if (refineStation.vigilanceCrues.cruesHistoriques == null) {
156 throw new VigiCruesException("No historical data available");
158 refineStation.vigilanceCrues.cruesHistoriques.stream()
159 .forEach(crue -> properties.putAll(crue.getDescription()));
160 String codeTerritoire = refineStation.vigilanceCrues.pereBoitEntVigiCru.cdEntVigiCru;
161 TerEntVigiCru territoire = apiHandler.getTerritoire(codeTerritoire);
162 for (VicANMoinsUn troncon : territoire.vicTerEntVigiCru.vicANMoinsUn) {
163 TronEntVigiCru detail = apiHandler.getTroncon(troncon.vicCdEntVigiCru);
164 if (detail.getStations().anyMatch(s -> config.id.equalsIgnoreCase(s.vicCdEntVigiCru))) {
165 properties.put(TRONCON, troncon.vicCdEntVigiCru);
169 } catch (VigiCruesException e) {
170 logger.info("Historical flooding data are not available {} : {}", config.id, e.getMessage());
171 channels.removeIf(channel -> channel.getUID().getId().contains(RELATIVE_PREFIX));
174 if (!properties.containsKey(TRONCON)) {
175 channels.removeIf(channel -> channel.getUID().getId().contains(ALERT));
176 channels.removeIf(channel -> channel.getUID().getId().contains(COMMENT));
180 OpenDatasoftResponse measures = apiHandler.getMeasures(config.id);
181 measures.getFirstRecord().ifPresent(field -> {
182 if (field.getHeight() == -1) {
183 channels.removeIf(channel -> (channel.getUID().getId().contains(HEIGHT)));
185 if (field.getFlow() == -1) {
186 channels.removeIf(channel -> (channel.getUID().getId().contains(FLOW)));
189 } catch (VigiCruesException e) {
190 logger.warn("Unable to read measurements data {} : {}", config.id, e.getMessage());
193 thingBuilder.withChannels(channels);
194 updateThing(thingBuilder.build());
200 public void dispose() {
201 logger.debug("Disposing the VigiCrues handler.");
203 ScheduledFuture<?> localJob = refreshJob;
204 if (localJob != null) {
205 localJob.cancel(true);
211 public void handleCommand(ChannelUID channelUID, Command command) {
212 if (command instanceof RefreshType) {
217 private void updateAndPublish() {
218 StationConfiguration config = getConfigAs(StationConfiguration.class);
220 OpenDatasoftResponse measures = apiHandler.getMeasures(config.id);
221 measures.getFirstRecord().ifPresent(field -> {
222 double height = field.getHeight();
224 updateQuantity(HEIGHT, height, SIUnits.METRE);
225 updateRelativeMeasure(RELATIVE_HEIGHT, referenceHeights, height);
228 double flow = field.getFlow();
230 updateQuantity(FLOW, flow, Units.CUBICMETRE_PER_SECOND);
231 updateRelativeMeasure(RELATIVE_FLOW, referenceFlows, flow);
234 field.getTimestamp().ifPresent(date -> updateDate(OBSERVATION_TIME, date));
236 String currentPortion = portion;
237 if (currentPortion != null) {
238 InfoVigiCru status = apiHandler.getTronconStatus(currentPortion);
239 updateAlert(ALERT, status.vicInfoVigiCru.vicNivInfoVigiCru - 1);
240 updateString(SHORT_COMMENT, status.vicInfoVigiCru.vicSituActuInfoVigiCru);
241 updateString(COMMENT, status.vicInfoVigiCru.vicQualifInfoVigiCru);
243 updateStatus(ThingStatus.ONLINE);
244 } catch (VigiCruesException e) {
245 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
249 private void updateString(String channelId, String value) {
250 if (isLinked(channelId)) {
251 updateState(channelId, new StringType(value));
255 private void updateRelativeMeasure(String channelId, List<QuantityType<?>> reference, double value) {
256 if (!reference.isEmpty()) {
257 double percent = value / reference.get(0).doubleValue() * 100;
258 updateQuantity(channelId, percent, Units.PERCENT);
262 private void updateQuantity(String channelId, Double value, Unit<?> unit) {
263 if (isLinked(channelId)) {
264 updateState(channelId, new QuantityType<>(value, unit));
268 private void updateDate(String channelId, ZonedDateTime zonedDateTime) {
269 if (isLinked(channelId)) {
270 updateState(channelId, new DateTimeType(zonedDateTime));
274 private void updateAlert(String channelId, int value) {
275 String channelIcon = channelId + "-icon";
276 if (isLinked(channelId)) {
277 updateState(channelId, new DecimalType(value));
279 if (isLinked(channelIcon)) {
280 byte[] resource = getResource(String.format("picto/crue-%d.svg", value));
281 updateState(channelIcon, resource != null ? new RawType(resource, "image/svg+xml") : UnDefType.UNDEF);
285 private byte @Nullable [] getResource(String iconPath) {
286 ClassLoader classLoader = VigiCruesHandler.class.getClassLoader();
287 if (classLoader != null) {
288 try (InputStream stream = classLoader.getResourceAsStream(iconPath)) {
289 return stream != null ? stream.readAllBytes() : null;
290 } catch (IOException e) {
291 logger.warn("Unable to load ressource '{}' : {}", iconPath, e.getMessage());