2 * Copyright (c) 2010-2023 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;
22 import java.util.Iterator;
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;
30 import javax.measure.Unit;
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;
65 * The {@link IpObserverHandler} is responsible for handling commands, which are
66 * sent to one of the channels.
68 * @author Thomas Hentschel - Initial contribution.
69 * @author Matthew Skinner - Full re-write for BND, V3.0 and UOM
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";
90 private class ChannelHandler {
91 private IpObserverHandler handler;
92 private Channel channel;
93 private String previousValue = "";
95 private final ArrayList<Class<? extends State>> acceptedDataTypes = new ArrayList<Class<? extends State>>();
97 ChannelHandler(IpObserverHandler handler, Channel channel, Class<? extends State> acceptable, Unit<?> unit) {
99 this.handler = handler;
100 this.channel = channel;
102 acceptedDataTypes.add(acceptable);
105 public void processValue(String sensorValue) {
106 if (!sensorValue.equals(previousValue)) {
107 previousValue = sensorValue;
108 switch (channel.getUID().getId()) {
109 case LAST_UPDATED_TIME:
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);
120 case OUTDOOR_BATTERY:
121 if ("1".equals(sensorValue)) {
122 handler.updateState(this.channel.getUID(), OnOffType.ON);
124 handler.updateState(this.channel.getUID(), OnOffType.OFF);
128 State state = TypeParser.parseState(this.acceptedDataTypes, sensorValue);
131 } else if (state instanceof QuantityType) {
132 handler.updateState(this.channel.getUID(),
133 QuantityType.valueOf(Double.parseDouble(sensorValue), unit));
135 handler.updateState(this.channel.getUID(), state);
141 public IpObserverHandler(Thing thing, HttpClient httpClient, IpObserverUpdateReceiver UpdateReceiver) {
143 this.httpClient = httpClient;
144 ipObserverUpdateReceiver = UpdateReceiver;
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.
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("=");
163 mappedQuery.put(pair.substring(0, index), pair.substring(index + 1, pair.length()));
166 handleServerReadings(mappedQuery);
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());
179 logger.trace("UNKNOWN element {}, value is {}", pair.getKey(), pair.getValue());
185 public void handleCommand(ChannelUID channelUID, Command command) {
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();
194 if ("1".equals(doc.select("select[name=u_Temperature] option[selected]").val())) {
195 imperialTemperature = true;
197 imperialTemperature = false;
200 if ("1".equals(doc.select("select[name=u_Rainfall] option[selected]").val())) {
203 imperialRain = false;
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);
214 value = doc.select("select[name=outBattSta] option[selected]").val();
215 localUpdater = channelHandlers.get("outBattSta");
216 if (localUpdater != null) {
217 localUpdater.processValue(value);
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);
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 = "";
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);
248 if (url == STATION_SETTINGS_URL) {
249 parseSettings(contentResponse.getContentAsString());
252 updateState(RESPONSE_TIME, new QuantityType<>(responseTime, MetricPrefix.MILLI(Units.SECOND)));
253 parseAndUpdate(contentResponse.getContentAsString());
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.",
258 sendGetRequest(REBOOT_URL);
262 errorReason = String.format("IpObserver request failed with %d: %s", contentResponse.getStatus(),
263 contentResponse.getReason());
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());
273 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, errorReason);
276 private void pollStation() {
277 sendGetRequest(LIVE_DATA_URL);
280 private void createChannelHandler(String chanName, Class<? extends State> type, Unit<?> unit, String htmlName) {
282 Channel channel = this.getThing().getChannel(chanName);
283 if (channel != null) {
284 channelHandlers.put(htmlName, new ChannelHandler(this, channel, type, unit));
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");
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");
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");
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");
327 logger.debug("Using metric units of measurement for rain.");
328 createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, MetricPrefix.MILLI(SIUnits.METRE),
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");
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");
358 "The IP Observer is sending a wind format the binding does not support. Select one of the other units.");
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");
367 "The IP Observer is sending fc (Foot Candles) for the solar radiation. Select one of the other units.");
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");
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");
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);
403 updateStatus(ThingStatus.UNKNOWN);
404 pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS);
409 public void dispose() {
410 ipObserverUpdateReceiver.removeStation(this);
411 channelHandlers.clear();
412 ScheduledFuture<?> localFuture = pollingFuture;
413 if (localFuture != null) {
414 localFuture.cancel(true);