]> git.basschouten.com Git - openhab-addons.git/blob
fe1f9da0da6b99e8db0574ec04da24a827fc2e7a
[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.ipobserver.internal;
14
15 import static org.openhab.binding.ipobserver.internal.IpObserverBindingConstants.*;
16
17 import java.time.ZonedDateTime;
18 import java.time.format.DateTimeFormatter;
19 import java.time.format.DateTimeParseException;
20 import java.util.ArrayList;
21 import java.util.HashMap;
22 import java.util.Iterator;
23 import java.util.Map;
24 import java.util.TimeZone;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.concurrent.TimeoutException;
29
30 import javax.measure.Unit;
31
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.eclipse.jetty.client.api.ContentResponse;
36 import org.eclipse.jetty.client.api.Request;
37 import org.eclipse.jetty.http.HttpHeader;
38 import org.eclipse.jetty.http.HttpMethod;
39 import org.jsoup.Jsoup;
40 import org.jsoup.nodes.Document;
41 import org.jsoup.nodes.Element;
42 import org.jsoup.select.Elements;
43 import org.openhab.core.library.types.DateTimeType;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.library.types.QuantityType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.library.unit.ImperialUnits;
49 import org.openhab.core.library.unit.MetricPrefix;
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.types.Command;
59 import org.openhab.core.types.State;
60 import org.openhab.core.types.TypeParser;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63
64 /**
65  * The {@link IpObserverHandler} is responsible for handling commands, which are
66  * sent to one of the channels.
67  *
68  * @author Thomas Hentschel - Initial contribution.
69  * @author Matthew Skinner - Full re-write for BND, V3.0 and UOM
70  */
71 @NonNullByDefault
72 public class IpObserverHandler extends BaseThingHandler {
73     private final HttpClient httpClient;
74     private final IpObserverUpdateReceiver ipObserverUpdateReceiver;
75     private final Logger logger = LoggerFactory.getLogger(IpObserverHandler.class);
76     private Map<String, ChannelHandler> channelHandlers = new HashMap<String, ChannelHandler>();
77     private @Nullable ScheduledFuture<?> pollingFuture = null;
78     private IpObserverConfiguration config = new IpObserverConfiguration();
79     private String idPass = "";
80     // Config settings parsed from weather station.
81     private boolean imperialTemperature = false;
82     private boolean imperialRain = false;
83     // 0=lux, 1=w/m2, 2=fc
84     private String solarUnit = "0";
85     // 0=m/s, 1=km/h, 2=ft/s, 3=bft, 4=mph, 5=knot
86     private String windUnit = "0";
87     // 0=hpa, 1=inhg, 2=mmhg
88     private String pressureUnit = "0";
89
90     private class ChannelHandler {
91         private IpObserverHandler handler;
92         private Channel channel;
93         private String previousValue = "";
94         private Unit<?> unit;
95         private final ArrayList<Class<? extends State>> acceptedDataTypes = new ArrayList<Class<? extends State>>();
96
97         ChannelHandler(IpObserverHandler handler, Channel channel, Class<? extends State> acceptable, Unit<?> unit) {
98             super();
99             this.handler = handler;
100             this.channel = channel;
101             this.unit = unit;
102             acceptedDataTypes.add(acceptable);
103         }
104
105         public void processValue(String sensorValue) {
106             if (!sensorValue.equals(previousValue)) {
107                 previousValue = sensorValue;
108                 switch (channel.getUID().getId()) {
109                     case LAST_UPDATED_TIME:
110                         try {
111                             DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm MM/dd/yyyy")
112                                     .withZone(TimeZone.getDefault().toZoneId());
113                             ZonedDateTime zonedDateTime = ZonedDateTime.parse(sensorValue, formatter);
114                             this.handler.updateState(this.channel.getUID(), new DateTimeType(zonedDateTime));
115                         } catch (DateTimeParseException e) {
116                             logger.debug("Could not parse {} as a valid dateTime", sensorValue);
117                         }
118                         return;
119                     case INDOOR_BATTERY:
120                     case OUTDOOR_BATTERY:
121                         if ("1".equals(sensorValue)) {
122                             handler.updateState(this.channel.getUID(), OnOffType.ON);
123                         } else {
124                             handler.updateState(this.channel.getUID(), OnOffType.OFF);
125                         }
126                         return;
127                 }
128                 State state = TypeParser.parseState(this.acceptedDataTypes, sensorValue);
129                 if (state == null) {
130                     return;
131                 } else if (state instanceof QuantityType) {
132                     handler.updateState(this.channel.getUID(),
133                             QuantityType.valueOf(Double.parseDouble(sensorValue), unit));
134                 } else {
135                     handler.updateState(this.channel.getUID(), state);
136                 }
137             }
138         }
139     }
140
141     public IpObserverHandler(Thing thing, HttpClient httpClient, IpObserverUpdateReceiver UpdateReceiver) {
142         super(thing);
143         this.httpClient = httpClient;
144         ipObserverUpdateReceiver = UpdateReceiver;
145     }
146
147     /**
148      * Takes a String of queries from the GET request made to the openHAB Jetty server and splits them
149      * into keys and values made up from the weather stations readings.
150      *
151      * @param update
152      */
153     public void processServerQuery(String update) {
154         if (update.startsWith(idPass)) {
155             String matchedUpdate = update.substring(idPass.length() + 1, update.length());
156             logger.trace("Update received:{}", matchedUpdate);
157             updateState(LAST_UPDATED_TIME, new DateTimeType(ZonedDateTime.now()));
158             Map<String, String> mappedQuery = new HashMap<>();
159             String[] readings = matchedUpdate.split("&");
160             for (String pair : readings) {
161                 int index = pair.indexOf("=");
162                 if (index > 0) {
163                     mappedQuery.put(pair.substring(0, index), pair.substring(index + 1, pair.length()));
164                 }
165             }
166             handleServerReadings(mappedQuery);
167         }
168     }
169
170     public void handleServerReadings(Map<String, String> updates) {
171         Iterator<?> it = updates.entrySet().iterator();
172         while (it.hasNext()) {
173             Map.Entry<?, ?> pair = (Map.Entry<?, ?>) it.next();
174             ChannelHandler localUpdater = channelHandlers.get(pair.getKey());
175             if (localUpdater != null) {
176                 logger.trace("Found element {}, value is {}", pair.getKey(), pair.getValue());
177                 localUpdater.processValue(pair.getValue().toString());
178             } else {
179                 logger.trace("UNKNOWN element {}, value is {}", pair.getKey(), pair.getValue());
180             }
181         }
182     }
183
184     @Override
185     public void handleCommand(ChannelUID channelUID, Command command) {
186     }
187
188     private void parseSettings(String html) {
189         Document doc = Jsoup.parse(html);
190         solarUnit = doc.select("select[name=unit_Solar] option[selected]").val();
191         windUnit = doc.select("select[name=unit_Wind] option[selected]").val();
192         pressureUnit = doc.select("select[name=unit_Pressure] option[selected]").val();
193         // 0=degC, 1=degF
194         if ("1".equals(doc.select("select[name=u_Temperature] option[selected]").val())) {
195             imperialTemperature = true;
196         } else {
197             imperialTemperature = false;
198         }
199         // 0=mm, 1=in
200         if ("1".equals(doc.select("select[name=u_Rainfall] option[selected]").val())) {
201             imperialRain = true;
202         } else {
203             imperialRain = false;
204         }
205     }
206
207     private void parseAndUpdate(String html) {
208         Document doc = Jsoup.parse(html);
209         String value = doc.select("select[name=inBattSta] option[selected]").val();
210         ChannelHandler localUpdater = channelHandlers.get("inBattSta");
211         if (localUpdater != null) {
212             localUpdater.processValue(value);
213         }
214         value = doc.select("select[name=outBattSta] option[selected]").val();
215         localUpdater = channelHandlers.get("outBattSta");
216         if (localUpdater != null) {
217             localUpdater.processValue(value);
218         }
219
220         Elements elements = doc.select("input");
221         for (Element element : elements) {
222             String elementName = element.attr("name");
223             value = element.attr("value");
224             if (!value.isEmpty()) {
225                 logger.trace("Found element {}, value is {}", elementName, value);
226                 localUpdater = channelHandlers.get(elementName);
227                 if (localUpdater != null) {
228                     localUpdater.processValue(value);
229                 }
230             }
231         }
232     }
233
234     private void sendGetRequest(String url) {
235         Request request = httpClient.newRequest("http://" + config.address + url);
236         request.method(HttpMethod.GET).timeout(5, TimeUnit.SECONDS).header(HttpHeader.ACCEPT_ENCODING, "gzip");
237         String errorReason = "";
238         try {
239             long start = System.currentTimeMillis();
240             ContentResponse contentResponse = request.send();
241             if (contentResponse.getStatus() == 200) {
242                 long responseTime = (System.currentTimeMillis() - start);
243                 if (!this.getThing().getStatus().equals(ThingStatus.ONLINE)) {
244                     updateStatus(ThingStatus.ONLINE);
245                     logger.debug("Finding out which units of measurement the weather station is using.");
246                     sendGetRequest(STATION_SETTINGS_URL);
247                 }
248                 if (url == STATION_SETTINGS_URL) {
249                     parseSettings(contentResponse.getContentAsString());
250                     setupChannels();
251                 } else {
252                     updateState(RESPONSE_TIME, new QuantityType<>(responseTime, MetricPrefix.MILLI(Units.SECOND)));
253                     parseAndUpdate(contentResponse.getContentAsString());
254                 }
255                 if (config.autoReboot > 0 && responseTime > config.autoReboot) {
256                     logger.debug("An Auto reboot of the IP Observer unit has been triggered as the response was {}ms.",
257                             responseTime);
258                     sendGetRequest(REBOOT_URL);
259                 }
260                 return;
261             } else {
262                 errorReason = String.format("IpObserver request failed with %d: %s", contentResponse.getStatus(),
263                         contentResponse.getReason());
264             }
265         } catch (TimeoutException e) {
266             errorReason = "TimeoutException: IpObserver was not reachable on your network";
267         } catch (ExecutionException e) {
268             errorReason = String.format("ExecutionException: %s", e.getMessage());
269         } catch (InterruptedException e) {
270             Thread.currentThread().interrupt();
271             errorReason = String.format("InterruptedException: %s", e.getMessage());
272         }
273         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorReason);
274     }
275
276     private void pollStation() {
277         sendGetRequest(LIVE_DATA_URL);
278     }
279
280     private void createChannelHandler(String chanName, Class<? extends State> type, Unit<?> unit, String htmlName) {
281         @Nullable
282         Channel channel = this.getThing().getChannel(chanName);
283         if (channel != null) {
284             channelHandlers.put(htmlName, new ChannelHandler(this, channel, type, unit));
285         }
286     }
287
288     private void setupServerChannels() {
289         createChannelHandler(WIND_DIRECTION, QuantityType.class, Units.DEGREE_ANGLE, "winddir");
290         createChannelHandler(INDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "indoorhumidity");
291         createChannelHandler(OUTDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "humidity");
292         createChannelHandler(TEMP_INDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "indoortempf");
293         createChannelHandler(TEMP_OUTDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "tempf");
294         createChannelHandler(TEMP_WIND_CHILL, QuantityType.class, ImperialUnits.FAHRENHEIT, "windchillf");
295         createChannelHandler(TEMP_DEW_POINT, QuantityType.class, ImperialUnits.FAHRENHEIT, "dewptf");
296         createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, ImperialUnits.INCH, "rainin");
297         createChannelHandler(DAILY_RAIN, QuantityType.class, ImperialUnits.INCH, "dailyrainin");
298         createChannelHandler(WEEKLY_RAIN, QuantityType.class, ImperialUnits.INCH, "weeklyrainin");
299         createChannelHandler(MONTHLY_RAIN, QuantityType.class, ImperialUnits.INCH, "monthlyrainin");
300         createChannelHandler(YEARLY_RAIN, QuantityType.class, ImperialUnits.INCH, "yearlyrainin");
301         createChannelHandler(UV_INDEX, DecimalType.class, SIUnits.CELSIUS, "UV");
302         createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "windspeedmph");
303         createChannelHandler(WIND_GUST, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "windgustmph");
304         createChannelHandler(SOLAR_RADIATION, QuantityType.class, Units.IRRADIANCE, "solarradiation");
305         createChannelHandler(REL_PRESSURE, QuantityType.class, ImperialUnits.INCH_OF_MERCURY, "baromin");
306         createChannelHandler(OUTDOOR_BATTERY, StringType.class, Units.PERCENT, "lowbatt");
307     }
308
309     private void setupChannels() {
310         if (imperialTemperature) {
311             logger.debug("Using imperial units of measurement for temperature.");
312             createChannelHandler(TEMP_INDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "inTemp");
313             createChannelHandler(TEMP_OUTDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "outTemp");
314         } else {
315             logger.debug("Using metric units of measurement for temperature.");
316             createChannelHandler(TEMP_INDOOR, QuantityType.class, SIUnits.CELSIUS, "inTemp");
317             createChannelHandler(TEMP_OUTDOOR, QuantityType.class, SIUnits.CELSIUS, "outTemp");
318         }
319
320         if (imperialRain) {
321             createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, ImperialUnits.INCH, "rainofhourly");
322             createChannelHandler(DAILY_RAIN, QuantityType.class, ImperialUnits.INCH, "rainofdaily");
323             createChannelHandler(WEEKLY_RAIN, QuantityType.class, ImperialUnits.INCH, "rainofweekly");
324             createChannelHandler(MONTHLY_RAIN, QuantityType.class, ImperialUnits.INCH, "rainofmonthly");
325             createChannelHandler(YEARLY_RAIN, QuantityType.class, ImperialUnits.INCH, "rainofyearly");
326         } else {
327             logger.debug("Using metric units of measurement for rain.");
328             createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE),
329                     "rainofhourly");
330             createChannelHandler(DAILY_RAIN, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE), "rainofdaily");
331             createChannelHandler(WEEKLY_RAIN, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE), "rainofweekly");
332             createChannelHandler(MONTHLY_RAIN, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE), "rainofmonthly");
333             createChannelHandler(YEARLY_RAIN, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE), "rainofyearly");
334         }
335
336         if ("5".equals(windUnit)) {
337             createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, Units.KNOT, "avgwind");
338             createChannelHandler(WIND_SPEED, QuantityType.class, Units.KNOT, "windspeed");
339             createChannelHandler(WIND_GUST, QuantityType.class, Units.KNOT, "gustspeed");
340             createChannelHandler(WIND_MAX_GUST, QuantityType.class, Units.KNOT, "dailygust");
341         } else if ("4".equals(windUnit)) {
342             createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "avgwind");
343             createChannelHandler(WIND_SPEED, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "windspeed");
344             createChannelHandler(WIND_GUST, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "gustspeed");
345             createChannelHandler(WIND_MAX_GUST, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "dailygust");
346         } else if ("1".equals(windUnit)) {
347             createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, SIUnits.KILOMETRE_PER_HOUR, "avgwind");
348             createChannelHandler(WIND_SPEED, QuantityType.class, SIUnits.KILOMETRE_PER_HOUR, "windspeed");
349             createChannelHandler(WIND_GUST, QuantityType.class, SIUnits.KILOMETRE_PER_HOUR, "gustspeed");
350             createChannelHandler(WIND_MAX_GUST, QuantityType.class, SIUnits.KILOMETRE_PER_HOUR, "dailygust");
351         } else if ("0".equals(windUnit)) {
352             createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, Units.METRE_PER_SECOND, "avgwind");
353             createChannelHandler(WIND_SPEED, QuantityType.class, Units.METRE_PER_SECOND, "windspeed");
354             createChannelHandler(WIND_GUST, QuantityType.class, Units.METRE_PER_SECOND, "gustspeed");
355             createChannelHandler(WIND_MAX_GUST, QuantityType.class, Units.METRE_PER_SECOND, "dailygust");
356         } else {
357             logger.warn(
358                     "The IP Observer is sending a wind format the binding does not support. Select one of the other units.");
359         }
360
361         if ("1".equals(solarUnit)) {
362             createChannelHandler(SOLAR_RADIATION, QuantityType.class, Units.IRRADIANCE, "solarrad");
363         } else if ("0".equals(solarUnit)) {
364             createChannelHandler(SOLAR_RADIATION, QuantityType.class, Units.LUX, "solarrad");
365         } else {
366             logger.warn(
367                     "The IP Observer is sending fc (Foot Candles) for the solar radiation. Select one of the other units.");
368         }
369
370         if ("0".equals(pressureUnit)) {
371             createChannelHandler(ABS_PRESSURE, QuantityType.class, MetricPrefix.HECTO(SIUnits.PASCAL), "AbsPress");
372             createChannelHandler(REL_PRESSURE, QuantityType.class, MetricPrefix.HECTO(SIUnits.PASCAL), "RelPress");
373         } else if ("1".equals(pressureUnit)) {
374             createChannelHandler(ABS_PRESSURE, QuantityType.class, ImperialUnits.INCH_OF_MERCURY, "AbsPress");
375             createChannelHandler(REL_PRESSURE, QuantityType.class, ImperialUnits.INCH_OF_MERCURY, "RelPress");
376         } else if ("2".equals(pressureUnit)) {
377             createChannelHandler(ABS_PRESSURE, QuantityType.class, Units.MILLIMETRE_OF_MERCURY, "AbsPress");
378             createChannelHandler(REL_PRESSURE, QuantityType.class, Units.MILLIMETRE_OF_MERCURY, "RelPress");
379         }
380
381         createChannelHandler(WIND_DIRECTION, QuantityType.class, Units.DEGREE_ANGLE, "windir");
382         createChannelHandler(INDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "inHumi");
383         createChannelHandler(OUTDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "outHumi");
384         // The units for the following are ignored as they are not a QuantityType.class
385         createChannelHandler(UV, DecimalType.class, SIUnits.CELSIUS, "uv");
386         createChannelHandler(UV_INDEX, DecimalType.class, SIUnits.CELSIUS, "uvi");
387         // was outBattSta1 so some units may use this instead?
388         createChannelHandler(OUTDOOR_BATTERY, StringType.class, Units.PERCENT, "outBattSta");
389         createChannelHandler(OUTDOOR_BATTERY, StringType.class, Units.PERCENT, "outBattSta1");
390         createChannelHandler(INDOOR_BATTERY, StringType.class, Units.PERCENT, "inBattSta");
391         createChannelHandler(LAST_UPDATED_TIME, DateTimeType.class, SIUnits.CELSIUS, "CurrTime");
392     }
393
394     @Override
395     public void initialize() {
396         config = getConfigAs(IpObserverConfiguration.class);
397         if (!config.id.isBlank() && !config.password.isBlank()) {
398             updateStatus(ThingStatus.ONLINE);
399             idPass = "ID=" + config.id + "&PASSWORD=" + config.password;
400             setupServerChannels();
401             ipObserverUpdateReceiver.addStation(this);
402         } else {
403             updateStatus(ThingStatus.UNKNOWN);
404             pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS);
405         }
406     }
407
408     @Override
409     public void dispose() {
410         ipObserverUpdateReceiver.removeStation(this);
411         channelHandlers.clear();
412         ScheduledFuture<?> localFuture = pollingFuture;
413         if (localFuture != null) {
414             localFuture.cancel(true);
415             localFuture = null;
416         }
417     }
418 }