]> git.basschouten.com Git - openhab-addons.git/blob
f60e581f8301cd644872956927a43c06dac4293f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.vigicrues.internal.handler;
14
15 import static org.openhab.binding.vigicrues.internal.VigiCruesBindingConstants.*;
16
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;
24 import java.util.Map;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.stream.Collectors;
28
29 import javax.measure.Unit;
30
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;
65
66 /**
67  * The {@link VigiCruesHandler} is responsible for querying the API and
68  * updating channels
69  *
70  * @author GaĆ«l L'hopital - Initial contribution
71  */
72 @NonNullByDefault
73 public class VigiCruesHandler extends BaseThingHandler {
74     private final Logger logger = LoggerFactory.getLogger(VigiCruesHandler.class);
75     private final LocationProvider locationProvider;
76
77     private @Nullable ScheduledFuture<?> refreshJob;
78     private final ApiHandler apiHandler;
79
80     private List<QuantityType<?>> referenceHeights = new ArrayList<>();
81     private List<QuantityType<?>> referenceFlows = new ArrayList<>();
82     private @Nullable String portion;
83
84     public VigiCruesHandler(Thing thing, LocationProvider locationProvider, ApiHandler apiHandler) {
85         super(thing);
86         this.apiHandler = apiHandler;
87         this.locationProvider = locationProvider;
88     }
89
90     @Override
91     public void initialize() {
92         logger.debug("Initializing VigiCrues handler.");
93
94         StationConfiguration config = getConfigAs(StationConfiguration.class);
95         logger.debug("config refresh = {} min", config.refresh);
96
97         updateStatus(ThingStatus.UNKNOWN);
98
99         if (thing.getProperties().isEmpty()) {
100             Map<String, String> properties = discoverAttributes(config);
101             updateProperties(properties);
102         }
103         getReferences();
104         refreshJob = scheduler.scheduleWithFixedDelay(this::updateAndPublish, 0, config.refresh, TimeUnit.MINUTES);
105     }
106
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);
112             if (value != null) {
113                 if (key.contains(FLOW)) {
114                     flows.add(new QuantityType<>(value));
115                 } else {
116                     heights.add(new QuantityType<>(value));
117                 }
118             }
119         });
120         referenceHeights = heights.stream().distinct().sorted().collect(Collectors.toList());
121         referenceFlows = flows.stream().distinct().sorted().collect(Collectors.toList());
122         portion = thing.getProperties().get(TRONCON);
123     }
124
125     private Map<String, String> discoverAttributes(StationConfiguration config) {
126         Map<String, String> properties = new HashMap<>();
127
128         ThingBuilder thingBuilder = editThing();
129         List<Channel> channels = new ArrayList<>(getThing().getChannels());
130
131         try {
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());
143                     }
144                     properties.put(RIVER, station.libelleCoursEau);
145                 });
146             } else {
147                 throw new VigiCruesException("No stations provided");
148             }
149         } catch (VigiCruesException e) {
150             logger.info("Unable to retrieve station location details {} : {}", config.id, e.getMessage());
151         }
152
153         try {
154             CdStationHydro refineStation = apiHandler.getStationDetails(config.id);
155             if (refineStation.vigilanceCrues.cruesHistoriques == null) {
156                 throw new VigiCruesException("No historical data available");
157             }
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);
166                     break;
167                 }
168             }
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));
172         }
173
174         if (!properties.containsKey(TRONCON)) {
175             channels.removeIf(channel -> channel.getUID().getId().contains(ALERT));
176             channels.removeIf(channel -> channel.getUID().getId().contains(COMMENT));
177         }
178
179         try {
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)));
184                 }
185                 if (field.getFlow() == -1) {
186                     channels.removeIf(channel -> (channel.getUID().getId().contains(FLOW)));
187                 }
188             });
189         } catch (VigiCruesException e) {
190             logger.warn("Unable to read measurements data {} : {}", config.id, e.getMessage());
191         }
192
193         thingBuilder.withChannels(channels);
194         updateThing(thingBuilder.build());
195
196         return properties;
197     }
198
199     @Override
200     public void dispose() {
201         logger.debug("Disposing the VigiCrues handler.");
202
203         ScheduledFuture<?> localJob = refreshJob;
204         if (localJob != null) {
205             localJob.cancel(true);
206         }
207         refreshJob = null;
208     }
209
210     @Override
211     public void handleCommand(ChannelUID channelUID, Command command) {
212         if (command instanceof RefreshType) {
213             updateAndPublish();
214         }
215     }
216
217     private void updateAndPublish() {
218         StationConfiguration config = getConfigAs(StationConfiguration.class);
219         try {
220             OpenDatasoftResponse measures = apiHandler.getMeasures(config.id);
221             measures.getFirstRecord().ifPresent(field -> {
222                 double height = field.getHeight();
223                 if (height != -1) {
224                     updateQuantity(HEIGHT, height, SIUnits.METRE);
225                     updateRelativeMeasure(RELATIVE_HEIGHT, referenceHeights, height);
226                 }
227
228                 double flow = field.getFlow();
229                 if (flow != -1) {
230                     updateQuantity(FLOW, flow, Units.CUBICMETRE_PER_SECOND);
231                     updateRelativeMeasure(RELATIVE_FLOW, referenceFlows, flow);
232                 }
233
234                 field.getTimestamp().ifPresent(date -> updateDate(OBSERVATION_TIME, date));
235             });
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);
242             }
243             updateStatus(ThingStatus.ONLINE);
244         } catch (VigiCruesException e) {
245             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
246         }
247     }
248
249     private void updateString(String channelId, String value) {
250         if (isLinked(channelId)) {
251             updateState(channelId, new StringType(value));
252         }
253     }
254
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);
259         }
260     }
261
262     private void updateQuantity(String channelId, Double value, Unit<?> unit) {
263         if (isLinked(channelId)) {
264             updateState(channelId, new QuantityType<>(value, unit));
265         }
266     }
267
268     private void updateDate(String channelId, ZonedDateTime zonedDateTime) {
269         if (isLinked(channelId)) {
270             updateState(channelId, new DateTimeType(zonedDateTime));
271         }
272     }
273
274     private void updateAlert(String channelId, int value) {
275         String channelIcon = channelId + "-icon";
276         if (isLinked(channelId)) {
277             updateState(channelId, new DecimalType(value));
278         }
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);
282         }
283     }
284
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());
292             }
293         }
294         return null;
295     }
296 }