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