2 * Copyright (c) 2010-2023 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.opendatasoft.OpenDatasoftResponse;
38 import org.openhab.binding.vigicrues.internal.dto.vigicrues.CdStationHydro;
39 import org.openhab.binding.vigicrues.internal.dto.vigicrues.InfoVigiCru;
40 import org.openhab.binding.vigicrues.internal.dto.vigicrues.TerEntVigiCru;
41 import org.openhab.binding.vigicrues.internal.dto.vigicrues.TronEntVigiCru;
42 import org.openhab.binding.vigicrues.internal.dto.vigicrues.VicANMoinsUn;
43 import org.openhab.core.i18n.LocationProvider;
44 import org.openhab.core.library.types.DateTimeType;
45 import org.openhab.core.library.types.DecimalType;
46 import org.openhab.core.library.types.PointType;
47 import org.openhab.core.library.types.QuantityType;
48 import org.openhab.core.library.types.RawType;
49 import org.openhab.core.library.types.StringType;
50 import org.openhab.core.library.unit.SIUnits;
51 import org.openhab.core.library.unit.Units;
52 import org.openhab.core.thing.Channel;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.binding.BaseThingHandler;
58 import org.openhab.core.thing.binding.builder.ThingBuilder;
59 import org.openhab.core.types.Command;
60 import org.openhab.core.types.RefreshType;
61 import org.openhab.core.types.UnDefType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
66 * The {@link VigiCruesHandler} is responsible for querying the API and
69 * @author Gaƫl L'hopital - Initial contribution
72 public class VigiCruesHandler extends BaseThingHandler {
73 private final Logger logger = LoggerFactory.getLogger(VigiCruesHandler.class);
74 private final LocationProvider locationProvider;
76 private @Nullable ScheduledFuture<?> refreshJob;
77 private final ApiHandler apiHandler;
79 private List<QuantityType<?>> referenceHeights = new ArrayList<>();
80 private List<QuantityType<?>> referenceFlows = new ArrayList<>();
81 private @Nullable String portion;
83 public VigiCruesHandler(Thing thing, LocationProvider locationProvider, ApiHandler apiHandler) {
85 this.apiHandler = apiHandler;
86 this.locationProvider = locationProvider;
90 public void initialize() {
91 logger.debug("Initializing VigiCrues handler.");
93 StationConfiguration config = getConfigAs(StationConfiguration.class);
94 logger.debug("config refresh = {} min", config.refresh);
96 updateStatus(ThingStatus.UNKNOWN);
98 if (thing.getProperties().isEmpty()) {
99 Map<String, String> properties = discoverAttributes(config);
100 updateProperties(properties);
103 refreshJob = scheduler.scheduleWithFixedDelay(this::updateAndPublish, 0, config.refresh, TimeUnit.MINUTES);
106 private void getReferences() {
107 List<QuantityType<?>> heights = new ArrayList<>();
108 List<QuantityType<?>> flows = new ArrayList<>();
109 thing.getProperties().keySet().stream().filter(key -> key.startsWith(FLOOD)).forEach(key -> {
110 String value = thing.getProperties().get(key);
112 if (key.contains(FLOW)) {
113 flows.add(new QuantityType<>(value));
115 heights.add(new QuantityType<>(value));
119 referenceHeights = heights.stream().distinct().sorted().collect(Collectors.toList());
120 referenceFlows = flows.stream().distinct().sorted().collect(Collectors.toList());
121 portion = thing.getProperties().get(TRONCON);
124 private Map<String, String> discoverAttributes(StationConfiguration config) {
125 Map<String, String> properties = new HashMap<>();
127 ThingBuilder thingBuilder = editThing();
128 List<Channel> channels = new ArrayList<>(getThing().getChannels());
131 HubEauResponse stationDetails = apiHandler.discoverStations(config.id);
132 stationDetails.stations.stream().findFirst().ifPresent(station -> {
133 PointType stationLocation = new PointType(
134 String.format(Locale.US, "%f,%f", station.latitudeStation, station.longitudeStation));
135 properties.put(LOCATION, stationLocation.toString());
136 PointType serverLocation = locationProvider.getLocation();
137 if (serverLocation != null) {
138 DecimalType distance = serverLocation.distanceFrom(stationLocation);
139 properties.put(DISTANCE, new QuantityType<>(distance, SIUnits.METRE).toString());
141 properties.put(RIVER, station.libelleCoursEau);
143 } catch (VigiCruesException e) {
144 logger.info("Unable to retrieve station location details {} : {}", config.id, e.getMessage());
148 CdStationHydro refineStation = apiHandler.getStationDetails(config.id);
149 if (refineStation.vigilanceCrues.cruesHistoriques == null) {
150 throw new VigiCruesException("No historical data available");
152 refineStation.vigilanceCrues.cruesHistoriques.stream()
153 .forEach(crue -> properties.putAll(crue.getDescription()));
154 String codeTerritoire = refineStation.vigilanceCrues.pereBoitEntVigiCru.cdEntVigiCru;
155 TerEntVigiCru territoire = apiHandler.getTerritoire(codeTerritoire);
156 for (VicANMoinsUn troncon : territoire.vicTerEntVigiCru.vicANMoinsUn) {
157 TronEntVigiCru detail = apiHandler.getTroncon(troncon.vicCdEntVigiCru);
158 if (detail.getStations().anyMatch(s -> config.id.equalsIgnoreCase(s.vicCdEntVigiCru))) {
159 properties.put(TRONCON, troncon.vicCdEntVigiCru);
163 } catch (VigiCruesException e) {
164 logger.info("Historical flooding data are not available {} : {}", config.id, e.getMessage());
165 channels.removeIf(channel -> channel.getUID().getId().contains(RELATIVE_PREFIX));
168 if (!properties.containsKey(TRONCON)) {
169 channels.removeIf(channel -> channel.getUID().getId().contains(ALERT));
170 channels.removeIf(channel -> channel.getUID().getId().contains(COMMENT));
174 OpenDatasoftResponse measures = apiHandler.getMeasures(config.id);
175 measures.getFirstRecord().ifPresent(field -> {
176 if (field.getHeight() == -1) {
177 channels.removeIf(channel -> (channel.getUID().getId().contains(HEIGHT)));
179 if (field.getFlow() == -1) {
180 channels.removeIf(channel -> (channel.getUID().getId().contains(FLOW)));
183 } catch (VigiCruesException e) {
184 logger.warn("Unable to read measurements data {} : {}", config.id, e.getMessage());
187 thingBuilder.withChannels(channels);
188 updateThing(thingBuilder.build());
194 public void dispose() {
195 logger.debug("Disposing the VigiCrues handler.");
197 ScheduledFuture<?> localJob = refreshJob;
198 if (localJob != null) {
199 localJob.cancel(true);
205 public void handleCommand(ChannelUID channelUID, Command command) {
206 if (command instanceof RefreshType) {
211 private void updateAndPublish() {
212 StationConfiguration config = getConfigAs(StationConfiguration.class);
214 OpenDatasoftResponse measures = apiHandler.getMeasures(config.id);
215 measures.getFirstRecord().ifPresent(field -> {
216 double height = field.getHeight();
218 updateQuantity(HEIGHT, height, SIUnits.METRE);
219 updateRelativeMeasure(RELATIVE_HEIGHT, referenceHeights, height);
222 double flow = field.getFlow();
224 updateQuantity(FLOW, flow, Units.CUBICMETRE_PER_SECOND);
225 updateRelativeMeasure(RELATIVE_FLOW, referenceFlows, flow);
228 field.getTimestamp().ifPresent(date -> updateDate(OBSERVATION_TIME, date));
230 String currentPortion = portion;
231 if (currentPortion != null) {
232 InfoVigiCru status = apiHandler.getTronconStatus(currentPortion);
233 updateAlert(ALERT, status.vicInfoVigiCru.vicNivInfoVigiCru - 1);
234 updateString(SHORT_COMMENT, status.vicInfoVigiCru.vicSituActuInfoVigiCru);
235 updateString(COMMENT, status.vicInfoVigiCru.vicQualifInfoVigiCru);
237 updateStatus(ThingStatus.ONLINE);
238 } catch (VigiCruesException e) {
239 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
243 private void updateString(String channelId, String value) {
244 if (isLinked(channelId)) {
245 updateState(channelId, new StringType(value));
249 private void updateRelativeMeasure(String channelId, List<QuantityType<?>> reference, double value) {
250 if (!reference.isEmpty()) {
251 double percent = value / reference.get(0).doubleValue() * 100;
252 updateQuantity(channelId, percent, Units.PERCENT);
256 private void updateQuantity(String channelId, Double value, Unit<?> unit) {
257 if (isLinked(channelId)) {
258 updateState(channelId, new QuantityType<>(value, unit));
262 private void updateDate(String channelId, ZonedDateTime zonedDateTime) {
263 if (isLinked(channelId)) {
264 updateState(channelId, new DateTimeType(zonedDateTime));
268 private void updateAlert(String channelId, int value) {
269 String channelIcon = channelId + "-icon";
270 if (isLinked(channelId)) {
271 updateState(channelId, new DecimalType(value));
273 if (isLinked(channelIcon)) {
274 byte[] resource = getResource(String.format("picto/crue-%d.svg", value));
275 updateState(channelIcon, resource != null ? new RawType(resource, "image/svg+xml") : UnDefType.UNDEF);
279 private byte @Nullable [] getResource(String iconPath) {
280 ClassLoader classLoader = VigiCruesHandler.class.getClassLoader();
281 if (classLoader != null) {
282 try (InputStream stream = classLoader.getResourceAsStream(iconPath)) {
283 return stream != null ? stream.readAllBytes() : null;
284 } catch (IOException e) {
285 logger.warn("Unable to load ressource '{}' : {}", iconPath, e.getMessage());