2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.ipobserver.internal;
15 import static org.openhab.binding.ipobserver.internal.IpObserverBindingConstants.*;
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;
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;
29 import javax.measure.Unit;
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;
64 * The {@link IpObserverHandler} is responsible for handling commands, which are
65 * sent to one of the channels.
67 * @author Thomas Hentschel - Initial contribution.
68 * @author Matthew Skinner - Full re-write for BND, V3.0 and UOM
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";
87 private class ChannelHandler {
88 private IpObserverHandler handler;
89 private Channel channel;
90 private String previousValue = "";
92 private final ArrayList<Class<? extends State>> acceptedDataTypes = new ArrayList<Class<? extends State>>();
94 ChannelHandler(IpObserverHandler handler, Channel channel, Class<? extends State> acceptable, Unit<?> unit) {
96 this.handler = handler;
97 this.channel = channel;
99 acceptedDataTypes.add(acceptable);
102 public void processValue(String sensorValue) {
103 if (!sensorValue.equals(previousValue)) {
104 previousValue = sensorValue;
105 switch (channel.getUID().getId()) {
106 case LAST_UPDATED_TIME:
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);
117 case OUTDOOR_BATTERY:
118 if ("1".equals(sensorValue)) {
119 handler.updateState(this.channel.getUID(), OnOffType.ON);
121 handler.updateState(this.channel.getUID(), OnOffType.OFF);
125 State state = TypeParser.parseState(this.acceptedDataTypes, sensorValue);
128 } else if (state instanceof QuantityType) {
129 handler.updateState(this.channel.getUID(),
130 QuantityType.valueOf(Double.parseDouble(sensorValue), unit));
132 handler.updateState(this.channel.getUID(), state);
138 public IpObserverHandler(Thing thing, HttpClient httpClient) {
140 this.httpClient = httpClient;
144 public void handleCommand(ChannelUID channelUID, Command command) {
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();
153 if ("1".equals(doc.select("select[name=u_Temperature] option[selected]").val())) {
154 imperialTemperature = true;
156 imperialTemperature = false;
159 if ("1".equals(doc.select("select[name=u_Rainfall] option[selected]").val())) {
162 imperialRain = false;
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);
173 value = doc.select("select[name=outBattSta] option[selected]").val();
174 localUpdater = channelHandlers.get("outBattSta");
175 if (localUpdater != null) {
176 localUpdater.processValue(value);
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);
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 = "";
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);
207 if (url == STATION_SETTINGS_URL) {
208 parseSettings(contentResponse.getContentAsString());
211 updateState(RESPONSE_TIME, new QuantityType<>(responseTime, MetricPrefix.MILLI(Units.SECOND)));
212 parseAndUpdate(contentResponse.getContentAsString());
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.",
217 sendGetRequest(REBOOT_URL);
221 errorReason = String.format("IpObserver request failed with %d: %s", contentResponse.getStatus(),
222 contentResponse.getReason());
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());
232 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorReason);
235 private void pollStation() {
236 sendGetRequest(LIVE_DATA_URL);
239 private void createChannelHandler(String chanName, Class<? extends State> type, Unit<?> unit, String htmlName) {
241 Channel channel = this.getThing().getChannel(chanName);
242 if (channel != null) {
243 channelHandlers.put(htmlName, new ChannelHandler(this, channel, type, unit));
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");
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");
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");
265 logger.debug("Using metric units of measurement for rain.");
266 createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE),
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");
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");
296 "The IP Observer is sending a wind format the binding does not support. Select one of the other units.");
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");
305 "The IP Observer is sending fc (Foot Candles) for the solar radiation. Select one of the other units.");
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");
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");
333 public void initialize() {
334 config = getConfigAs(IpObserverConfiguration.class);
335 updateStatus(ThingStatus.UNKNOWN);
336 pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS);
340 public void dispose() {
341 channelHandlers.clear();
342 ScheduledFuture<?> localFuture = pollingFuture;
343 if (localFuture != null) {
344 localFuture.cancel(true);