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