]> git.basschouten.com Git - openhab-addons.git/blob
c1300e0ca14e35e14dfe989c28397e79521eaa2a
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.meteoalerte.internal.handler;
14
15 import static org.openhab.binding.meteoalerte.internal.MeteoAlerteBindingConstants.*;
16
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.net.MalformedURLException;
20 import java.nio.charset.StandardCharsets;
21 import java.time.ZonedDateTime;
22 import java.util.AbstractMap;
23 import java.util.Map;
24 import java.util.Objects;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.meteoalerte.internal.MeteoAlerteConfiguration;
31 import org.openhab.binding.meteoalerte.internal.json.ApiResponse;
32 import org.openhab.binding.meteoalerte.internal.json.ResponseFieldDTO.AlertLevel;
33 import org.openhab.core.io.net.http.HttpUtil;
34 import org.openhab.core.library.types.DateTimeType;
35 import org.openhab.core.library.types.DecimalType;
36 import org.openhab.core.library.types.RawType;
37 import org.openhab.core.library.types.StringType;
38 import org.openhab.core.thing.ChannelUID;
39 import org.openhab.core.thing.Thing;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.binding.BaseThingHandler;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import com.google.gson.Gson;
51
52 /**
53  * The {@link MeteoAlerteHandler} is responsible for updating channels
54  * and querying the API
55  *
56  * @author Gaël L'hopital - Initial contribution
57  */
58 @NonNullByDefault
59 public class MeteoAlerteHandler extends BaseThingHandler {
60     private static final String URL = "https://public.opendatasoft.com/api/records/1.0/search/?dataset=risques-meteorologiques-copy&"
61             + "facet=etat_vent&facet=etat_pluie_inondation&facet=etat_orage&facet=etat_inondation&facet=etat_neige&facet=etat_canicule&"
62             + "facet=etat_grand_froid&facet=etat_avalanches&refine.nom_dept=%s";
63     private static final int TIMEOUT_MS = 30000;
64     private static final String UNKNOWN_COLOR = "b3b3b3";
65     private static final Map<AlertLevel, String> ALERT_COLORS = Map.ofEntries(
66             new AbstractMap.SimpleEntry<AlertLevel, String>(AlertLevel.GREEN, "00ff00"),
67             new AbstractMap.SimpleEntry<AlertLevel, String>(AlertLevel.YELLOW, "ffff00"),
68             new AbstractMap.SimpleEntry<AlertLevel, String>(AlertLevel.ORANGE, "ff6600"),
69             new AbstractMap.SimpleEntry<AlertLevel, String>(AlertLevel.RED, "ff0000"),
70             new AbstractMap.SimpleEntry<AlertLevel, String>(AlertLevel.UNKNOWN, UNKNOWN_COLOR));
71
72     private final Logger logger = LoggerFactory.getLogger(MeteoAlerteHandler.class);
73     // Time zone provider representing time zone configured in openHAB configuration
74     private final Gson gson;
75     private @Nullable ScheduledFuture<?> refreshJob;
76     private String queryUrl = "";
77
78     public MeteoAlerteHandler(Thing thing, Gson gson) {
79         super(thing);
80         this.gson = gson;
81     }
82
83     @Override
84     public void initialize() {
85         logger.debug("Initializing Météo Alerte handler.");
86
87         MeteoAlerteConfiguration config = getConfigAs(MeteoAlerteConfiguration.class);
88         logger.debug("config department = {}", config.department);
89         logger.debug("config refresh = {}", config.refresh);
90
91         updateStatus(ThingStatus.UNKNOWN);
92         queryUrl = String.format(URL, config.department);
93         refreshJob = scheduler.scheduleWithFixedDelay(this::updateAndPublish, 0, config.refresh, TimeUnit.MINUTES);
94     }
95
96     @Override
97     public void dispose() {
98         logger.debug("Disposing the Météo Alerte handler.");
99         ScheduledFuture<?> refreshJob = this.refreshJob;
100         if (refreshJob != null) {
101             refreshJob.cancel(true);
102         }
103         this.refreshJob = null;
104     }
105
106     @Override
107     public void handleCommand(ChannelUID channelUID, Command command) {
108         if (command instanceof RefreshType) {
109             updateAndPublish();
110         }
111     }
112
113     private void updateAndPublish() {
114         try {
115             if (queryUrl.isEmpty()) {
116                 throw new MalformedURLException("queryUrl not initialized");
117             }
118             String response = HttpUtil.executeUrl("GET", queryUrl, TIMEOUT_MS);
119             if (response == null) {
120                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "empty response");
121                 return;
122             }
123             updateStatus(ThingStatus.ONLINE);
124             ApiResponse apiResponse = gson.fromJson(response, ApiResponse.class);
125             updateChannels(Objects.requireNonNull(apiResponse));
126         } catch (MalformedURLException e) {
127             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
128                     String.format("Querying '%s' raised : %s", queryUrl, e.getMessage()));
129         } catch (IOException e) {
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
131         }
132     }
133
134     /**
135      * Update the channel from the last Meteo Alerte data retrieved
136      *
137      * @param channelId the id identifying the channel to be updated
138      */
139     private void updateChannels(ApiResponse apiResponse) {
140         apiResponse.getRecords().findFirst().ifPresent((record) -> record.getResponseFieldDTO().ifPresent(fields -> {
141             updateAlert(WIND, fields.getVent());
142             updateAlert(RAIN, fields.getPluieInondation());
143             updateAlert(STORM, fields.getOrage());
144             updateAlert(FLOOD, fields.getInondation());
145             updateAlert(SNOW, fields.getNeige());
146             updateAlert(HEAT, fields.getCanicule());
147             updateAlert(FREEZE, fields.getGrandFroid());
148             updateAlert(AVALANCHE, fields.getAvalanches());
149             updateAlert(WAVE, fields.getVagueSubmersion());
150             updateState(COMMENT, new StringType(fields.getVigilanceComment()));
151             fields.getDateInsert().ifPresent(date -> updateDate(OBSERVATION_TIME, date));
152             fields.getDatePrevue().ifPresent(date -> updateDate(END_TIME, date));
153         }));
154     }
155
156     public byte @Nullable [] getResource(String iconPath) {
157         ClassLoader classLoader = MeteoAlerteHandler.class.getClassLoader();
158         if (classLoader != null) {
159             try (InputStream stream = classLoader.getResourceAsStream(iconPath)) {
160                 return stream != null ? stream.readAllBytes() : null;
161             } catch (IOException e) {
162                 logger.warn("Unable to load ressource '{}' : {}", iconPath, e.getMessage());
163             }
164         }
165         return null;
166     }
167
168     public void updateAlert(String channelId, AlertLevel value) {
169         String channelIcon = channelId + "-icon";
170         if (isLinked(channelId)) {
171             updateState(channelId, getAlertLevel(value));
172         }
173         if (isLinked(channelIcon)) {
174             State result = UnDefType.UNDEF;
175             byte[] bytes = getResource(String.format("picto/%s.svg", channelId));
176             if (bytes != null) {
177                 String resource = new String(bytes, StandardCharsets.UTF_8);
178                 resource = resource.replaceAll(UNKNOWN_COLOR, ALERT_COLORS.getOrDefault(value, UNKNOWN_COLOR));
179                 result = new RawType(resource.getBytes(StandardCharsets.UTF_8), "image/svg+xml");
180             }
181             updateState(channelIcon, result);
182         }
183     }
184
185     public void updateDate(String channelId, ZonedDateTime zonedDateTime) {
186         if (isLinked(channelId)) {
187             updateState(channelId, new DateTimeType(zonedDateTime));
188         }
189     }
190
191     public State getAlertLevel(AlertLevel alert) {
192         switch (alert) {
193             case GREEN:
194                 return DecimalType.ZERO;
195             case YELLOW:
196                 return new DecimalType(1);
197             case ORANGE:
198                 return new DecimalType(2);
199             case RED:
200                 return new DecimalType(3);
201             default:
202                 return UnDefType.UNDEF;
203         }
204     }
205 }