]> git.basschouten.com Git - openhab-addons.git/commitdiff
[ipobserver] Add support for WiFi version and push based method. (#12151)
authorMatthew Skinner <matt@pcmus.com>
Sat, 12 Feb 2022 08:34:16 +0000 (19:34 +1100)
committerGitHub <noreply@github.com>
Sat, 12 Feb 2022 08:34:16 +0000 (09:34 +0100)
* Add support for Wifi version of ipObserver.
* make config private again.
* Add logging.
* Remove tags

Signed-off-by: Matthew Skinner <matt@pcmus.com>
bundles/org.openhab.binding.ipobserver/README.md
bundles/org.openhab.binding.ipobserver/src/main/java/org/openhab/binding/ipobserver/internal/IpObserverBindingConstants.java
bundles/org.openhab.binding.ipobserver/src/main/java/org/openhab/binding/ipobserver/internal/IpObserverConfiguration.java
bundles/org.openhab.binding.ipobserver/src/main/java/org/openhab/binding/ipobserver/internal/IpObserverHandler.java
bundles/org.openhab.binding.ipobserver/src/main/java/org/openhab/binding/ipobserver/internal/IpObserverHandlerFactory.java
bundles/org.openhab.binding.ipobserver/src/main/java/org/openhab/binding/ipobserver/internal/IpObserverUpdateReceiver.java [new file with mode: 0644]
bundles/org.openhab.binding.ipobserver/src/main/resources/OH-INF/i18n/ipobserver.properties
bundles/org.openhab.binding.ipobserver/src/main/resources/OH-INF/thing/thing-types.xml

index 4e7e39497ebfb46d3703ddbaf2173aaa5330d39b..100586090039a6a012acf34cbf9631db06fb3a08 100644 (file)
@@ -4,9 +4,14 @@ This binding is for any weather station that sends data to an IP Observer module
 The weather stations that do this are made by a company in China called `Fine Offset` and then re-branded by many distribution companies around the world.
 Some of the brands include Aercus (433mhz), Ambient Weather (915mhz), Frogitt, Misol (433mhz), Pantech (433mhz), Sainlogic and many more.
 Whilst Ambient Weather has it own cloud based binding, the other brands will not work with that binding and Ambient Weather do not sell outside of the United States.
-This binding works fully offline and uses local scraping of the weather station data at 12 second resolution if you wish and is easy to setup.
+
+This binding works fully offline and can work via one of two methods:
+
+1. Local scraping of the weather station's `livedata` webpage at 12 second resolution (non WiFi models only).
+2. Both WiFi and RJ45 models can be setup to push the data directly to the openHAB (default 8080) server directly and the binding can parse the data from the weather underground data.
+
 The other binding worth mentioning is the weather underground binding that allows the data to be intercepted on its way to WU, however many of the weather stations do not allow the redirection of the WU data and require you to know how to do redirections with a custom DNS server on your network.
-This binding is by far the easiest method and works for all the brands and will not stop the data still being sent to WU if you wish to do both at the same time.
+This binding with method 1 and a RJ45 model is by far the easiest method and works for all the brands and will not stop the data still being sent to WU if you wish to do both at the same time.
 If your weather station came with a LCD screen instead of the IP Observer, you can add on the unit and the LCD screen will still work in parallel as the RF data is sent 1 way from the outdoor unit to the inside screens and IP Observer units.
 
 ## Supported Things
@@ -15,15 +20,21 @@ There is only one thing that can be added and is called `weatherstation`.
 
 ## Discovery
 
-Auto discovery is supported and may take a while to complete as it scans all IP addresses on your network one by one.
+Auto discovery is supported for the RJ45 models, while the WiFi IP Observer will need to be manually added.
+Discovery may take a while to complete as it scans all IP addresses on your network one by one.
 
 ## Thing Configuration
 
+When the id and password are supplied, you need to set the custom WU path to `/weatherstation/updateweatherstation.php` and the port to be the same as openHAB (port 8080 by default).
+If they are left blank, the binding will work in the scraping mode (RJ45 model only).
+
 | Parameter | Required | Description |
 |-|-|-|
 | `address` | Y | Hostname or IP for the IP Observer |
 | `pollTime` | Y | Time in seconds between each Scan of the livedata.htm from the IP Observer |
 | `autoReboot` | Y | Time in milliseconds to wait for a reply before rebooting the IP Observer. A value of 0 disables this feature allowing you to manually trigger or use a rule to handle the reboots. |
+| `id` | N | The weather underground's `station ID` that is setup in the ipobservers settings. |
+| `password` | N | The weather underground's `station key` that is setup in the ipobservers settings. |
 
 ## Channels
 
index d923fba9ef00cd62c67c1f5d016da5090ffe6620..60d50cf73fb45b55c1825c0ae1ae1a2c979c1f00 100644 (file)
@@ -26,6 +26,7 @@ public class IpObserverBindingConstants {
     public static final String BINDING_ID = "ipobserver";
     public static final String REBOOT_URL = "/msgreboot.htm";
     public static final String LIVE_DATA_URL = "/livedata.htm";
+    public static final String SERVER_UPDATE_URL = "/weatherstation/updateweatherstation.php";
     public static final String STATION_SETTINGS_URL = "/station.htm";
     public static final int DISCOVERY_THREAD_POOL_SIZE = 15;
 
@@ -35,6 +36,8 @@ public class IpObserverBindingConstants {
     // List of all Channel ids
     public static final String TEMP_INDOOR = "temperatureIndoor";
     public static final String TEMP_OUTDOOR = "temperatureOutdoor";
+    public static final String TEMP_WIND_CHILL = "temperatureWindChill";
+    public static final String TEMP_DEW_POINT = "temperatureDewPoint";
     public static final String INDOOR_HUMIDITY = "humidityIndoor";
     public static final String OUTDOOR_HUMIDITY = "humidityOutdoor";
     public static final String ABS_PRESSURE = "pressureAbsolute";
index 0c7f61400b6b35fda6b701875531d5fe851552a3..aaff7a10fc18f5e73d16f92027cd8c9280fb50c4 100644 (file)
@@ -24,4 +24,6 @@ public class IpObserverConfiguration {
     public String address = "";
     public int pollTime = 20;
     public int autoReboot = 2000;
+    public String password = "";
+    public String id = "";
 }
index 36990b8e7b8b33bb2175384a0c58e5b5f64b3157..c6e473645333472453273b1c46896e13a15c9111 100644 (file)
@@ -19,6 +19,7 @@ import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.TimeZone;
 import java.util.concurrent.ExecutionException;
@@ -70,10 +71,12 @@ import org.slf4j.LoggerFactory;
 @NonNullByDefault
 public class IpObserverHandler extends BaseThingHandler {
     private final HttpClient httpClient;
+    private final IpObserverUpdateReceiver ipObserverUpdateReceiver;
     private final Logger logger = LoggerFactory.getLogger(IpObserverHandler.class);
     private Map<String, ChannelHandler> channelHandlers = new HashMap<String, ChannelHandler>();
     private @Nullable ScheduledFuture<?> pollingFuture = null;
     private IpObserverConfiguration config = new IpObserverConfiguration();
+    private String idPass = "";
     // Config settings parsed from weather station.
     private boolean imperialTemperature = false;
     private boolean imperialRain = false;
@@ -135,9 +138,47 @@ public class IpObserverHandler extends BaseThingHandler {
         }
     }
 
-    public IpObserverHandler(Thing thing, HttpClient httpClient) {
+    public IpObserverHandler(Thing thing, HttpClient httpClient, IpObserverUpdateReceiver UpdateReceiver) {
         super(thing);
         this.httpClient = httpClient;
+        ipObserverUpdateReceiver = UpdateReceiver;
+    }
+
+    /**
+     * Takes a String of queries from the GET request made to the openHAB Jetty server and splits them
+     * into keys and values made up from the weather stations readings.
+     *
+     * @param update
+     */
+    public void processServerQuery(String update) {
+        if (update.startsWith(idPass)) {
+            String matchedUpdate = update.substring(idPass.length() + 1, update.length());
+            logger.trace("Update received:{}", matchedUpdate);
+            updateState(LAST_UPDATED_TIME, new DateTimeType(ZonedDateTime.now()));
+            Map<String, String> mappedQuery = new HashMap<>();
+            String[] readings = matchedUpdate.split("&");
+            for (String pair : readings) {
+                int index = pair.indexOf("=");
+                if (index > 0) {
+                    mappedQuery.put(pair.substring(0, index), pair.substring(index + 1, pair.length()));
+                }
+            }
+            handleServerReadings(mappedQuery);
+        }
+    }
+
+    public void handleServerReadings(Map<String, String> updates) {
+        Iterator<?> it = updates.entrySet().iterator();
+        while (it.hasNext()) {
+            Map.Entry<?, ?> pair = (Map.Entry<?, ?>) it.next();
+            ChannelHandler localUpdater = channelHandlers.get(pair.getKey());
+            if (localUpdater != null) {
+                logger.trace("Found element {}, value is {}", pair.getKey(), pair.getValue());
+                localUpdater.processValue(pair.getValue().toString());
+            } else {
+                logger.trace("UNKNOWN element {}, value is {}", pair.getKey(), pair.getValue());
+            }
+        }
     }
 
     @Override
@@ -244,6 +285,27 @@ public class IpObserverHandler extends BaseThingHandler {
         }
     }
 
+    private void setupServerChannels() {
+        createChannelHandler(WIND_DIRECTION, QuantityType.class, Units.DEGREE_ANGLE, "winddir");
+        createChannelHandler(INDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "indoorhumidity");
+        createChannelHandler(OUTDOOR_HUMIDITY, DecimalType.class, Units.PERCENT, "humidity");
+        createChannelHandler(TEMP_INDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "indoortempf");
+        createChannelHandler(TEMP_OUTDOOR, QuantityType.class, ImperialUnits.FAHRENHEIT, "tempf");
+        createChannelHandler(TEMP_WIND_CHILL, QuantityType.class, ImperialUnits.FAHRENHEIT, "windchillf");
+        createChannelHandler(TEMP_DEW_POINT, QuantityType.class, ImperialUnits.FAHRENHEIT, "dewptf");
+        createChannelHandler(HOURLY_RAIN_RATE, QuantityType.class, ImperialUnits.INCH, "rainin");
+        createChannelHandler(DAILY_RAIN, QuantityType.class, ImperialUnits.INCH, "dailyrainin");
+        createChannelHandler(WEEKLY_RAIN, QuantityType.class, ImperialUnits.INCH, "weeklyrainin");
+        createChannelHandler(MONTHLY_RAIN, QuantityType.class, ImperialUnits.INCH, "monthlyrainin");
+        createChannelHandler(YEARLY_RAIN, QuantityType.class, ImperialUnits.INCH, "yearlyrainin");
+        createChannelHandler(UV_INDEX, DecimalType.class, SIUnits.CELSIUS, "UV");
+        createChannelHandler(WIND_AVERAGE_SPEED, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "windspeedmph");
+        createChannelHandler(WIND_GUST, QuantityType.class, ImperialUnits.MILES_PER_HOUR, "windgustmph");
+        createChannelHandler(SOLAR_RADIATION, QuantityType.class, Units.IRRADIANCE, "solarradiation");
+        createChannelHandler(REL_PRESSURE, QuantityType.class, ImperialUnits.INCH_OF_MERCURY, "baromin");
+        createChannelHandler(OUTDOOR_BATTERY, StringType.class, Units.PERCENT, "lowbatt");
+    }
+
     private void setupChannels() {
         if (imperialTemperature) {
             logger.debug("Using imperial units of measurement for temperature.");
@@ -332,12 +394,20 @@ public class IpObserverHandler extends BaseThingHandler {
     @Override
     public void initialize() {
         config = getConfigAs(IpObserverConfiguration.class);
-        updateStatus(ThingStatus.UNKNOWN);
-        pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS);
+        if (!config.id.isBlank() && !config.password.isBlank()) {
+            updateStatus(ThingStatus.ONLINE);
+            idPass = "ID=" + config.id + "&PASSWORD=" + config.password;
+            setupServerChannels();
+            ipObserverUpdateReceiver.addStation(this);
+        } else {
+            updateStatus(ThingStatus.UNKNOWN);
+            pollingFuture = scheduler.scheduleWithFixedDelay(this::pollStation, 1, config.pollTime, TimeUnit.SECONDS);
+        }
     }
 
     @Override
     public void dispose() {
+        ipObserverUpdateReceiver.removeStation(this);
         channelHandlers.clear();
         ScheduledFuture<?> localFuture = pollingFuture;
         if (localFuture != null) {
index 2d22aba0fc027b5d8a1700134d8bb280b4f10655..6e5c120e683632afa023e40ff9ea3263f7618366 100644 (file)
@@ -28,6 +28,7 @@ import org.openhab.core.thing.binding.ThingHandlerFactory;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
+import org.osgi.service.http.HttpService;
 
 /**
  * The {@link IpObserverHandlerFactory} is responsible for creating things and thing
@@ -39,11 +40,14 @@ import org.osgi.service.component.annotations.Reference;
 @Component(configurationPid = "binding.ipobserver", service = ThingHandlerFactory.class)
 public class IpObserverHandlerFactory extends BaseThingHandlerFactory {
     private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_WEATHER_STATION);
+    private final IpObserverUpdateReceiver ipObserverUpdateReceiver;
     protected final HttpClient httpClient;
 
     @Activate
-    public IpObserverHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
+    public IpObserverHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+            @Reference HttpService httpService) {
         this.httpClient = httpClientFactory.getCommonHttpClient();
+        ipObserverUpdateReceiver = new IpObserverUpdateReceiver(httpService);
     }
 
     protected HttpClient getHttpClient() {
@@ -60,7 +64,7 @@ public class IpObserverHandlerFactory extends BaseThingHandlerFactory {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
 
         if (THING_WEATHER_STATION.equals(thingTypeUID)) {
-            return new IpObserverHandler(thing, httpClient);
+            return new IpObserverHandler(thing, httpClient, ipObserverUpdateReceiver);
         }
 
         return null;
diff --git a/bundles/org.openhab.binding.ipobserver/src/main/java/org/openhab/binding/ipobserver/internal/IpObserverUpdateReceiver.java b/bundles/org.openhab.binding.ipobserver/src/main/java/org/openhab/binding/ipobserver/internal/IpObserverUpdateReceiver.java
new file mode 100644 (file)
index 0000000..5622911
--- /dev/null
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2010-2022 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.ipobserver.internal;
+
+import static org.openhab.binding.ipobserver.internal.IpObserverBindingConstants.SERVER_UPDATE_URL;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.osgi.service.http.HttpService;
+import org.osgi.service.http.NamespaceException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link IpObserverUpdateReceiver} captures any updates sent to the openHAB Jetty server if the weather station is
+ * setup to direct the weather updates to the HTTP server of openHAB which is normally port 8080.
+ *
+ * @author Matthew Skinner - Initial contribution
+ */
+@NonNullByDefault
+public class IpObserverUpdateReceiver extends HttpServlet {
+    private static final long serialVersionUID = -234658674L;
+    private final Logger logger = LoggerFactory.getLogger(this.getClass());
+    private List<IpObserverHandler> listOfHandlers = new ArrayList<>(1);
+
+    public IpObserverUpdateReceiver(HttpService httpService) {
+        try {
+            httpService.registerServlet(SERVER_UPDATE_URL, this, null, httpService.createDefaultHttpContext());
+        } catch (NamespaceException | ServletException e) {
+            logger.warn("Registering servlet failed:{}", e.getMessage());
+        }
+    }
+
+    @Override
+    protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
+        if (req == null) {
+            return;
+        }
+        String stationUpdate = req.getQueryString();
+        if (stationUpdate == null) {
+            return;
+        }
+        logger.debug("Weather station packet received from {}", req.getRemoteHost());
+        for (IpObserverHandler ipObserverHandler : listOfHandlers) {
+            ipObserverHandler.processServerQuery(stationUpdate);
+        }
+    }
+
+    public void addStation(IpObserverHandler ipObserverHandler) {
+        listOfHandlers.add(ipObserverHandler);
+    }
+
+    public void removeStation(IpObserverHandler ipObserverHandler) {
+        listOfHandlers.remove(ipObserverHandler);
+    }
+}
index d515eb12e1749f8b371b9098f41353523006c0af..6e9dd69bb265d7e07261057106010dfa8c606b42 100644 (file)
@@ -14,6 +14,10 @@ thing-type.config.ipobserver.weatherstation.address.label = Network Address
 thing-type.config.ipobserver.weatherstation.address.description = Hostname or IP for the IP Observer
 thing-type.config.ipobserver.weatherstation.autoReboot.label = Auto Reboot
 thing-type.config.ipobserver.weatherstation.autoReboot.description = Time in milliseconds to wait for a reply before rebooting the IP Observer. A value of 0 disables this feature allowing you to manually trigger or use a rule to handle the reboots
+thing-type.config.ipobserver.weatherstation.id.label = Station ID
+thing-type.config.ipobserver.weatherstation.id.description = The station ID used to connect to WeatherUnderGround. Leave blank if you wish to poll the livedata.
+thing-type.config.ipobserver.weatherstation.password.label = Station Password
+thing-type.config.ipobserver.weatherstation.password.description = The station password used to connect to WeatherUnderGround. Leave blank if you wish to poll the livedata.
 thing-type.config.ipobserver.weatherstation.pollTime.label = Poll Time
 thing-type.config.ipobserver.weatherstation.pollTime.description = Time in seconds between each Scan of the livedata.htm from the ObserverIP
 
@@ -41,8 +45,12 @@ channel-type.ipobserver.responseTime.label = Response Time
 channel-type.ipobserver.responseTime.description = How many milliseconds it took to fetch the sensor readings from livedata.htm
 channel-type.ipobserver.solarRadiation.label = Solar Radiation
 channel-type.ipobserver.solarRadiation.description = Solar Radiation
+channel-type.ipobserver.temperatureDewPoint.label = Dew Point Temperature
+channel-type.ipobserver.temperatureDewPoint.description = Dew Point Temperature Outdoors
 channel-type.ipobserver.temperatureIndoor.label = Indoor Temperature
 channel-type.ipobserver.temperatureIndoor.description = Current Temperature Indoors
+channel-type.ipobserver.temperatureWindChill.label = Wind Chill Temperature
+channel-type.ipobserver.temperatureWindChill.description = Wind Chill Temperature Outdoors
 channel-type.ipobserver.uv.label = UV
 channel-type.ipobserver.uv.description = UV
 channel-type.ipobserver.uvIndex.label = UV Index
index 8fdd9f2e3e6960849f626e99b50a441ce3f81902..561a656bb127b2f610b3162f395016a6cc329597 100644 (file)
@@ -10,6 +10,8 @@
                <channels>
                        <channel id="temperatureIndoor" typeId="temperatureIndoor"/>
                        <channel id="temperatureOutdoor" typeId="system.outdoor-temperature"/>
+                       <channel id="temperatureWindChill" typeId="temperatureWindChill"/>
+                       <channel id="temperatureDewPoint" typeId="temperatureDewPoint"/>
                        <channel id="humidityIndoor" typeId="humidityIndoor"/>
                        <channel id="humidityOutdoor" typeId="system.atmospheric-humidity"/>
                        <channel id="pressureAbsolute" typeId="pressureAbsolute"/>
                                        feature allowing you to manually trigger or use a rule to handle the reboots</description>
                                <default>2000</default>
                        </parameter>
+                       <parameter name="id" type="text">
+                               <label>Station ID</label>
+                               <description>The station ID used to connect to WeatherUnderGround. Leave blank if you wish to poll the livedata.</description>
+                       </parameter>
+                       <parameter name="password" type="text">
+                               <context>password</context>
+                               <label>Station Password</label>
+                               <description>The station password used to connect to WeatherUnderGround. Leave blank if you wish to poll the
+                                       livedata.</description>
+                       </parameter>
                </config-description>
        </thing-type>
        <channel-type id="responseTime" advanced="true">
                </tags>
                <state pattern="%.1f %unit%" readOnly="true"/>
        </channel-type>
+       <channel-type id="temperatureWindChill" advanced="true">
+               <item-type>Number:Temperature</item-type>
+               <label>Wind Chill Temperature</label>
+               <description>Wind Chill Temperature Outdoors</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
+       <channel-type id="temperatureDewPoint" advanced="true">
+               <item-type>Number:Temperature</item-type>
+               <label>Dew Point Temperature</label>
+               <description>Dew Point Temperature Outdoors</description>
+               <category>Temperature</category>
+               <state pattern="%.1f %unit%" readOnly="true"/>
+       </channel-type>
        <channel-type id="humidityIndoor">
                <item-type>Number:Dimensionless</item-type>
                <label>Indoor Humidity</label>
                <label>Wind Max Gust</label>
                <description>Max wind gust for today</description>
                <category>Wind</category>
-               <tags>
-                       <tag>Measurement</tag>
-                       <tag>Wind</tag>
-               </tags>
                <state pattern="%.1f %unit%" readOnly="true"/>
        </channel-type>
        <channel-type id="rainHourlyRate">
                <label>Rain for Week</label>
                <description>Weekly Rain</description>
                <category>Rain</category>
-               <tags>
-                       <tag>Measurement</tag>
-                       <tag>Rain</tag>
-               </tags>
                <state pattern="%.2f %unit%" readOnly="true"/>
        </channel-type>
        <channel-type id="rainForMonth" advanced="true">
                <label>Rain for Month</label>
                <description>Rain since 12:00 on the 1st of this month</description>
                <category>Rain</category>
-               <tags>
-                       <tag>Measurement</tag>
-                       <tag>Rain</tag>
-               </tags>
                <state pattern="%.2f %unit%" readOnly="true"/>
        </channel-type>
        <channel-type id="rainForYear">
                <label>Rain for Year</label>
                <description>Total rain since 12:00 on 1st Jan</description>
                <category>Rain</category>
-               <tags>
-                       <tag>Measurement</tag>
-                       <tag>Rain</tag>
-               </tags>
                <state pattern="%.2f %unit%" readOnly="true"/>
        </channel-type>
        <channel-type id="lastUpdatedTime" advanced="true">