[wundergroundupdatereceiver] Initial contribution (#10105)

* [wundergroundupdatereceiver] Initial implementation

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Code review

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Fix some description formatting

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Add some more channel types per request

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Add more headers to response



Signed-off-by: Daniel Demus <daniel-github@demus.dk>
Based on the observed headers from actual traffic to wunderground.com.

* [wundergroundupdatereceiver] Discovery service

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Upgrade to 3.2, fix group name constants

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Add a list of channel types

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Add dateutc as synthetic DateTime channel

Also add som emore constants
Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Do more programmatic configuration

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] More readme

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Update copyright year

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Handle multiple instance request parameters

Signed-off-by: Daniel Demus <daniel-github@demus.dk>
Ie. those that can have an index number in the name, fx. temp1f, temp2f

* [wundergroundupdatereceiver] Add unmapped but mappable channels

To support multiple devices any new parameters that are submitted cause a new channel to be created
Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Don't pass superfluous config to channels

Also documentation additions
Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Add generated i18n file

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Re-case test method names

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

* [wundergroundupdatereceiver] Re-word thing file section of README

Signed-off-by: Daniel Demus <daniel-github@demus.dk>

Co-authored-by: Daniel Demus <dde@nine.dk>
This commit is contained in:
Daniel Demus 2022-06-19 15:01:54 +00:00 committed by GitHub
parent 3a632ca659
commit 56f4c8943b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 3245 additions and 0 deletions

View File

@ -353,6 +353,7 @@
/bundles/org.openhab.binding.wlanthermo/ @CSchlipp
/bundles/org.openhab.binding.wled/ @Skinah
/bundles/org.openhab.binding.wolfsmartset/ @BoBiene
/bundles/org.openhab.binding.wundergroundupdatereceiver/ @danieldemus
/bundles/org.openhab.binding.xmltv/ @clinique
/bundles/org.openhab.binding.xmppclient/ @pavel-gololobov
/bundles/org.openhab.binding.yamahamusiccast/ @coop-git

View File

@ -1766,6 +1766,11 @@
<artifactId>org.openhab.binding.wolfsmartset</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.wundergroundupdatereceiver</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.xmltv</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,229 @@
# Wunderground Update Receiver Binding
Many personal weather stations or similar devices are only capable of submitting measurements to the wunderground.com update site.
This binding enables acting as a receiver of updates from devices that post measurements to https://rtupdate.wunderground.com/weatherstation/updateweatherstation.php.
If the hostname is configurable - as on weather stations based on the Fine Offset Electronics WH2600-IP - this is simple, otherwise you have to set up dns such that it resolves the above hostname to your server, without preventing the server from resolving the proper ip if you want to forward the request.
The server thus listens at http(s)://<your-openHAB-server>:<openHAB-port>/weatherstation/updateweatherstation.php and the device needs to be pointed at this address.
If you can't configure the device itself to submit to an alternate hostname you would need to set up a dns server that resolves rtupdate.wunderground.com to the IP-address of your server and provide as dns to the device does DHCP.
Make sure not to use this dns server instance for any other DHCP clients.
The request is in itself simple to parse, so by redirecting it to your openHAB server you can intercept the values and use them to control items in your home.
E.g. use measured wind-speed to close an awning or turn on the sprinkler system after some time without rain.
This binding allows you to mix and match products from various manufacturers that otherwise have a closed system.
If you wish to pass the measurements on to rtupdate.wunderground.com, you can use a simple rule that triggers on the `wundergroundupdatereceiver:wundergroundUpdateReceiver:<channel-id>:metadata#last-query-trigger` to do so.
It can also be used to submit the same measurements to multiple weather services via multiple rules.
## Supported Things
Any device that sends weather measurement updates to the wunderground.com update URLs is supported.
It is easiest to use with devices that have a configurable target address, but can be made to work with any internet-connected device, that gets its dns server via DHCP or where the DNS server can be set.
## Discovery
The binding starts listening at the above-mentioned URI as soon as it is initialized.
Any request with an unregistered stationId is recorded and if auto-discovery is enabled appears in the inbox, otherwise can be registered when a manual scan is initiated.
For each request parameter a channel is generated, based on a list of known parameters from https://support.weather.com/s/article/PWS-Upload-Protocol?language=en_US and other observed parameters from various devices.
If you have a device that submits a parameter that is unknown in the current version of the binding please feel free to submit an issue to have it added.
While discovery is active, either in the background or during a manual scan, any request parameters that don't have a channel associated with them are added to the thing's channels.
This supports using multiple devices that submit measurements to the same station ID.
The thing is the wunderground account, not the individual devices submitting measurements.
## Thing Configuration
The only configurable value is the station id, which should match the one configured on the device.
If you don't plan on submitting measurements to wunderground.com, it can be any unique non-empty string value, otherwise it must be the actual station ID.
## Channels
Each measurement type the wunderground.com update service accepts has a channel.
The channels must be named exactly as the request parameter they receive.
I.e. the wind speed channel must be named `windspeedmph` as that is the request parameter name defined by Wunderground in their API.
The channel name set up in the binding should be considered an id with no semantic content other than pointing to the wounderground API.
Additionally there is a receipt timestamp and a trigger channel.
### Request parameters are mapped to one of the following channel-types:
#### Normal channel-types:
| Request parameter | Channel type id | Type | Label | Description | Group |
|-------------------|------------------------------|----------------------|--------------------------------|----------------------------------------------------------------------------------------|-------------|
| winddir | wind-direction | Number:Angle | Current Wind Direction | Current wind direction | Wind |
| windspeedmph | wind-speed | Number:Speed | Current Wind Speed | Current wind speed, using software specific time period. | Wind |
| windgustmph | wind-gust-speed | Number:Speed | Current Gust Speed | Current wind gust speed, using software specific time period. | Wind |
| windgustdir | wind-gust-direction | Number:Angle | Gust Direction | Current wind gust direction expressed as an angle using software specific time period. | Wind |
| tempf | temperature | Number:Temperature | Outdoor Temperature | Current outdoor temperature | Temperature |
| indoortempf | indoor-temperature | Number:Temperature | Indoor Temperature | Indoor temperature. | Temperature |
| rainin | rain | Number:Length | Hourly Rain | Rain over the past hour. | Rain |
| dailyrainin | rain-daily | Number:Length | Daily Rain | Rain since the start of the day. | Rain |
| solarradiation | solarradiation | Number:Intensity | Solar Radiation | Solar radiation | Sun |
| UV | uv | Number:Dimensionless | UV Index | UV index. | Sun |
| humidity | humidity | Number:Dimensionless | Humidity | Humidity in %. | Humidity |
| indoorhumidity | indoor-humidity | Number:Dimensionless | Indoor Humidity | Indoor humidity in %. | Humidity |
| baromin |
#### Advanced channel-types:
| Request parameter | Channel type id | Type | Label | Description | Group |
|-------------------|------------------------------|----------------------|--------------------------------|-----------------------------------------------------------------------------------------------------|-------------|
| windspdmph_avg2m | wind-speed-avg-2min | Number:Speed | Wind Speed 2min Average | 2 minute average wind speed. | Wind |
| winddir_avg2m | wind-direction-avg-2min | Number:Angle | Wind Direction 2min Average | 2 minute average wind direction. | Wind |
| windgustmph_10m | wind-gust-speed-10min | Number:Speed | Gust Speed 10min Average | 10 minute average gust speed. | Wind |
| windgustdir_10m | wind-gust-direction-10min | Number:Angle | Gust Direction 10min Average | 10 minute average gust direction. | Wind |
| windchillf | wind-chill | Number:Temperature | Wind Chill | The apparent wind chill temperature. | Temperature |
| soiltempf | soil-temperature | Number:Temperature | Soil Temperature | Soil temperature. | Temperature |
| weeklyrainin | rain-weekly | Number:Length | Weekly Rain | Rain since the start of this week. | Rain |
| monthlyrainin | rain-monthly | Number:Length | Monthly Rain | Rain since the start if this month. | Rain |
| yearlyrainin | rain-yearly | Number:Length | Yearly Rain | Rain since the start of this year. | Rain |
| weather | metar | String | METAR Weather Report | METAR formatted weather report | Sun_Clouds |
| clouds | clouds | String | Cloud Cover | METAR style cloud cover. | Sun_Clouds |
| visibility | visibility | Number:Length | Visibility | Visibility. | Sun_Clouds |
| dewptf | dew-point | Number:Temperature | Dew Point | Outdoor dew point. | Humidity |
| soilmoisture | soil-moisture | Number:Dimensionless | Soil Moisture | Soil moisture in %. | Moisture |
| leafwetness | leafwetness | Number:Dimensionless | Leaf Wetness | Leaf wetness in %. | Moisture |
| AqNO | nitric-oxide | Number:Dimensionless | Nitric Oxide | Nitric Oxide ppm. | Pollution |
| AqNO2T | nitrogen-dioxide-measured | Number:Dimensionless | Nitrogen Dioxide | Nitrogen Dioxide, true measure ppb. | Pollution |
| AqNO2 | nitrogen-dioxide-nox-no | Number:Dimensionless | NO2 X computed | NO2 computed, NOx-NO ppb. | Pollution |
| AqNO2Y | nitrogen-dioxide-noy-no | Number:Dimensionless | NO2 Y computed, NOy-NO ppb | NO2 computed, NOy-NO ppb. | Pollution |
| AqNOX | nitrogen-oxides | Number:Dimensionless | Nitrogen Oxides | Nitrogen Oxides ppb. | Pollution |
| AqNOY | total-reactive-nitrogen | Number:Dimensionless | Total Reactive Nitrogen | Total reactive nitrogen. | Pollution |
| AqNO3 | no3-ion | Number:Density | NO3 ion | NO3 ion (nitrate, not adjusted for ammonium ion) µG/m3. | Pollution |
| AqSO4 | so4-ion | Number:Density | SO4 ion | SO4 ion (sulfate, not adjusted for ammonium ion) µG/m3. | Pollution |
| AqSO2 | sulfur-dioxide | Number:Dimensionless | Sulfur Dioxide | Sulfur Dioxide, conventional ppb. | Pollution |
| AqSO2T | sulfur-dioxide-trace-levels | Number:Dimensionless | Sulfur Dioxide Trace Levels | Sulfur Dioxide, trace levels ppb. | Pollution |
| AqCO | carbon-monoxide | Number:Dimensionless | Carbon Monoxide | Carbon Monoxide, conventional ppm. | Pollution |
| AqCOT | carbon-monoxide-trace-levels | Number:Dimensionless | Carbon Monoxide Trace Levels | Carbon Monoxide, trace levels ppb. | Pollution |
| AqEC | elemental-carbon | Number:Density | Elemental Carbon | Elemental Carbon, PM2.5 µG/m3. | Pollution |
| AqOC | organic-carbon | Number:Density | Organic Carbon | Organic Carbon, not adjusted for oxygen and hydrogen, PM2.5 µG/m3. | Pollution |
| AqBC | black-carbon | Number:Density | Black Carbon | Black Carbon at 880 nm, µG/m3. | Pollution |
| AqUV-AETH | aethalometer | Number:Density | Second Channel of Aethalometer | second channel of Aethalometer at 370 nm, µG/m3. | Pollution |
| AqPM2.5 | pm2_5-mass | Number:Density | PM2.5 Mass | PM2.5 mass, µG/m3. | Pollution |
| AqPM10 | pm10-mass | Number:Density | PM10 Mass | PM10 mass, µG/m3. | Pollution |
| AqOZONE | ozone | Number:Dimensionless | Ozone | Ozone, ppb. | Pollution |
#### Metadata channel-types:
| Request parameter | Channel type id | Type | Label | Description | Group |
|-------------------|------------------------------|----------------------|-----------------------------------|-----------------------------------------------------------------------------------------------------|-------------|
| dateutc | dateutc | String | Last Updated | The date and time of the last update in UTC as submitted by the weather station. This can be 'now'. | Metadata |
| softwaretype | softwaretype | String | Software Type | A software type string from the weather station | Metadata |
| rtfreq | realtime-frequency | Number | Realtime Frequency | How often does the weather station submit measurements | Metadata |
| lowbatt | system:low-battery | Switch | Low Battery | Low battery warning with possible values on (low battery) and off (battery ok) | Metadata |
#### Synthetic channel-types. These are programmatically added:
| Channel type id | Type | Channel type | Label | Description | Group |
|------------------------|----------------------|--------------|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|
| dateutc-datetime | dateutc-datetime | state | Last Updated as DateTime | The date and time of the last update in UTC as submitted by the weather station converted to a DateTime value. In case of 'now', the current time is used. | Metadata |
| last-received-datetime | DateTime | state | Last Received | The date and time of the last update. | Metadata |
| last-query-state | String | state | The last query | The part of the last query after the first unurlencoded '?' | Metadata |
| last-query-trigger | String | trigger | The last query | The part of the last query after the first unurlencoded '?' | Metadata |
The trigger channel's payload is the last querystring, so the following dsl rule script would send the measurements on to wunderground.com:
```
val requestQuery = receivedEvent
sendHttpGetRequest("https://rtupdate.wunderground.com/weatherstation/updateweatherstation.php?" + requestQuery)
```
The PASSWORD, action and realtime parameters are ignored, as they are wunderground technical constants, that devices must send.
The ID parameter is used to identify the correct thing the request pertains to, i.e. the stationId configuration value.
As described by the wunderground specification a device can submit multiple values for the outdoor temperature, soil temperature, soil moisture and leaf wetness channels by inserting an index number into the name of the request parameter, fx. tempf can be temp1f, temp2f, etc.
This is supported by the discovery mechanism, creating a channel for each of the values.
# Examples
### Thing file
Configuration using thing and item files is not the recommended method, as you have to manually replicate the configuration discovery produces.
Channels _must_ be named as the request parameters in the channel type table, otherwise the binding will not be able to update with values from requests.
So the request parameter names submitted by your particular device(s) need to be found before being able to write appropriate thing files.
You need to intercept a request from your devices(s) using something like wireshark.
Both thing and item files must be created manually to produce a result practically identical to the one produced through automatic discovery.
Assuming you have intercepted a request such as `https://rtupdate.wunderground.com/weatherstation/updateweatherstation.php?ID=MYSTATIONID&PASSWORD=XXXXXX&windspeedmph=3.11&dateutc=2021-02-07%2014:04:03&softwaretype=WH2600%20V2.2.8&action=updateraw&realtime=1&rtfreq=5`, you can configure a thing to intercept the request thus:
```
Thing wundergroundupdatereceiver:wundergroundUpdateReceiver:ATHINGID "Foo" [stationId="MYSTATIONID"] {
Channels:
Type wind-speed : windspeedmph []
Type dateutc : dateutc []
Type softwaretype : softwaretype []
Type realtime-frequency : rtfreq []
Type dateutc-datetime : dateutc-datetime []
Type last-received-datetime : last-received-datetime []
Type last-query-state : last-query-state []
Type last-query-trigger : last-query-trigger []
}
```
The pattern for a given channel is `Type <channel type id> : <request paramter> []` from the channel types table.
Casing of the request parameter is significant.
None of the current channels take config.
### Item file
```
Number:Speed WuBinding_WeatherStation_WindSpeed "Current Wind Speed [%.2f %unit%]" <wind> { channel="wundergroundupdatereceiver:wundergroundUpdateReceiver:ATHINGID:windspeedmph" }
DateTime WuBinding_LastRecieved "Last Recieved Time [%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS]" <time> { channel="wundergroundupdatereceiver:wundergroundUpdateReceiver:ATHINGID:last-received-datetime" }
```
The binding tries to post received values as the item types described in the channel types table, so attaching a channel that takes a given type to an item of a different type has undefined behaviour.
### Rule examples
You can use the trigger channel to create a rule to calculate additional values.
Create an new manual Item with a meaningful id, fx. WundergroundUpdateReceiverBinging_HeatIndex with a Number type.
Create a rule that triggers when the trigger channel is updated and the following DSL:
```
if ( (WH2k6_OutdoorTemperature.state != NULL) && (WH2k6_OutdoorTemperature.state != UNDEF) &&
(WH2k6_Humidity.state != NULL) && (WH2k6_Humidity.state != UNDEF)
)
{
val Double tempf = (WH2k6_OutdoorTemperature.state as QuantityType<Number>).toUnit("°F").doubleValue
val Double humidity = (WH2k6_Humidity.state as QuantityType<Number>).toUnit("%").doubleValue
// https://www.wpc.ncep.noaa.gov/html/heatindex_equation.shtml
if ( tempf>=80.0 ) {
var Double heatindex = -42.379 + 2.04901523*tempf + 10.14333127*humidity - 0.22475541*tempf*humidity - 0.00683783*tempf*tempf - 0.05481717*humidity*humidity + 0.00122874*tempf*tempf*humidity + 0.00085282*tempf*humidity*humidity - 0.00000199*tempf*tempf*humidity*humidity;
if ( (humidity <= 13.0) && (tempf >= 80.0) && (tempf <= 112.0) ) {
heatindex = heatindex - ((13.0 - humidity)/4.0) * (Math::sqrt(17.0-Math::abs(tempf-95.0)/17.0));
} else if ( (humidity >= 85.0) && (tempf >= 80.0) && (tempf <= 87.0) ) {
heatindex = heatindex + ((humidity - 85.0)/10.0)*((87.0-tempf)/5.0);
}
WundergroundUpdateReceiverBinging_HeatIndex.postUpdate(heatindex + "°F")
} else {
WundergroundUpdateReceiverBinging_HeatIndex.postUpdate(UNDEF)
}
}
```
You would then have to trigger another rule to submit the original request with any calculated values appended.
You can also define a transformation to fx. get a cardinal direction (N, S, W, E):
```
(function(s){
if ( (s == "NULL") || (s == "UNDEF") )
{
return undefined;
}
else
{
var dir = ["N ⬇️", "NNO ⬇️", "NO ↙️", "ONO ⬅️", "O ⬅️", "OSO ⬅️", "SO ↖️", "SSO ⬆️", "S ⬆️", "SSW ⬆️", "SW ↗️", "WSW ➡️", "W ➡️", "WNW ➡️", "NW ↘️", "NNW ⬇️"]; var wind = parseInt(s.split(" ")[0]);
var winddiroffset = (wind + (360.0/32.0)) % 360.0;
var winddiridx = Math.floor(winddiroffset / (360.0/16.0));
var winddir = dir[winddiridx];
return winddir + ' ('+ wind +'°)';
}
})(input)
```
The examples were kindly provided by MikeTheTux.

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.wundergroundupdatereceiver</artifactId>
<name>openHAB Add-ons :: Bundles :: Wunderground Update Receiver Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.wundergroundupdatereceiver-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-wundergroundupdatereceiver" description="Wunderground Update Receiver Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.wundergroundupdatereceiver/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,228 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static org.openhab.core.library.unit.SIUnits.METRE;
import java.util.Set;
import javax.measure.Unit;
import javax.measure.quantity.Length;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.type.ChannelTypeUID;
import tech.units.indriya.format.SimpleUnitFormat;
import tech.units.indriya.function.MultiplyConverter;
import tech.units.indriya.unit.TransformedUnit;
/**
* The {@link WundergroundUpdateReceiverBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault
public class WundergroundUpdateReceiverBindingConstants {
public static final String BINDING_ID = "wundergroundupdatereceiver";
public static final String STATION_ID_PARAMETER = "ID";
public static final String REPRESENTATION_PROPERTY = "stationId";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_UPDATE_RECEIVER = new ThingTypeUID(BINDING_ID,
"wundergroundUpdateReceiver");
static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_UPDATE_RECEIVER);
public static final String NOW = "now";
// Excluded technical paramter names
public static final String REALTIME_MARKER = "realtime";
public static final String PASSWORD = "PASSWORD";
public static final String ACTION = "action";
// List of default synthetic channeltypes added to a new thing
public static final String DATEUTC_DATETIME = "dateutc-datetime";
public static final String LAST_RECEIVED_DATETIME = "last-received-datetime";
public static final String LAST_RECEIVED = "last-received";
public static final String LAST_QUERY = "last-query";
public static final String LAST_QUERY_STATE = LAST_QUERY + "-state";
public static final String LAST_QUERY_TRIGGER = LAST_QUERY + "-trigger";
// Channel groups
public static final String METADATA_GROUP = "metadata";
public static final String WIND_GROUP = "wind";
public static final String TEMPERATURE_GROUP = "temperature";
public static final String HUMIDITY_GROUP = "humidity";
public static final String RAIN_GROUP = "rain";
public static final String SUNLIGHT_GROUP = "sunlight";
public static final String PRESSURE_GROUP = "pressure";
public static final String POLLUTION_GROUP = "pollution";
// Known or observed request paramters received from weather stations submitting to wunderground.com
public static final String DATEUTC = "dateutc";
public static final String SOFTWARE_TYPE = "softwaretype";
public static final String LOW_BATTERY = "lowbatt";
public static final String REALTIME_FREQUENCY = "rtfreq";
public static final String WIND_DIRECTION = "winddir";
public static final String WIND_SPEED = "windspeedmph";
public static final String GUST_SPEED = "windgustmph";
public static final String GUST_DIRECTION = "windgustdir";
public static final String WIND_SPEED_AVG_2MIN = "windspdmph_avg2m";
public static final String WIND_DIRECTION_AVG_2MIN = "winddir_avg2m";
public static final String GUST_SPEED_AVG_10MIN = "windgustmph_10m";
public static final String GUST_DIRECTION_AVG_10MIN = "windgustdir_10m";
public static final String TEMPERATURE = "tempf";
public static final String TEMPERATURE_INDEXED = "^temp(\\d+)f$";
public static final String WIND_CHILL = "windchillf";
public static final String INDOOR_TEMPERATURE = "indoortempf";
public static final String SOIL_TEMPERATURE = "soiltempf";
public static final String SOIL_TEMPERATURE_INDEXED = "^soiltemp(\\d+)f$";
public static final String HUMIDITY = "humidity";
public static final String INDOOR_HUMIDITY = "indoorhumidity";
public static final String DEWPOINT = "dewptf";
public static final String SOIL_MOISTURE = "soilmoisture";
public static final String SOIL_MOISTURE_INDEXED = "^soilmoisture(\\d+)$";
public static final String LEAF_WETNESS = "leafwetness";
public static final String LEAF_WETNESS_INDEXED = "^leafwetness(\\d+)$";
public static final String RAIN_IN = "rainin";
public static final String DAILY_RAIN_IN = "dailyrainin";
public static final String WEEKLY_RAIN_IN = "weeklyrainin";
public static final String MONTHLY_RAIN_IN = "monthlyrainin";
public static final String YEARLY_RAIN_IN = "yearlyrainin";
public static final String SOLAR_RADIATION = "solarradiation";
public static final String UV = "UV";
public static final String VISIBILITY = "visibility";
public static final String WEATHER = "weather";
public static final String CLOUDS = "clouds";
public static final String BAROM_IN = "baromin";
public static final String AQ_NO = "AqNO";
public static final String AQ_NO2T = "AqNO2T";
public static final String AQ_NO2 = "AqNO2";
public static final String AQ_NO2Y = "AqNO2Y";
public static final String AQ_NOX = "AqNOX";
public static final String AQ_NOY = "AqNOY";
public static final String AQ_NO3 = "AqNO3";
public static final String AQ_SO4 = "AqSO4";
public static final String AQ_SO2 = "AqSO2";
public static final String AQ_SO2T = "AqSO2T";
public static final String AQ_CO = "AqCO";
public static final String AQ_COT = "AqCOT";
public static final String AQ_EC = "AqEC";
public static final String AQ_OC = "AqOC";
public static final String AQ_BC = "AqBC";
public static final String AQ_UV_AETH = "AqUV-AETH";
public static final String AQ_PM2_5 = "AqPM2.5";
public static final String AQ_PM10 = "AqPM10";
public static final String AQ_OZONE = "AqOZONE";
public static final ChannelTypeUID LAST_RECEIVED_DATETIME_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
LAST_RECEIVED_DATETIME);
public static final ChannelTypeUID LAST_QUERY_STATE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
LAST_QUERY_STATE);
public static final ChannelTypeUID LAST_QUERY_TRIGGER_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
LAST_QUERY_TRIGGER);
public static final ChannelTypeUID DATEUTC_DATETIME_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
DATEUTC_DATETIME);
public static final ChannelTypeUID DATEUTC_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, DATEUTC);
public static final ChannelTypeUID LOW_BATTERY_CHANNELTYPEUID = DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_LOW_BATTERY
.getUID();
public static final ChannelTypeUID SOFTWARETYPE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "softwaretype");
public static final ChannelTypeUID REALTIME_FREQUENCY_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"realtime-frequency");
public static final ChannelTypeUID WIND_SPEED_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "wind-speed");
public static final ChannelTypeUID WIND_DIRECTION_CHANNELTYPEUID = DefaultSystemChannelTypeProvider.SYSTEM_WIND_DIRECTION
.getUID();
public static final ChannelTypeUID GUST_SPEED_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "wind-gust-speed");
public static final ChannelTypeUID GUST_DIRECTION_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"wind-gust-direction");
public static final ChannelTypeUID WIND_SPEED_AVG_2MIN_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"wind-speed-avg-2min");
public static final ChannelTypeUID WIND_DIRECTION_AVG_2MIN_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"wind-direction-avg-2min");
public static final ChannelTypeUID GUST_SPEED_10MIN_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"wind-gust-speed-10min");
public static final ChannelTypeUID GUST_DIRECTION_10MIN_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"wind-gust-direction-10min");
public static final ChannelTypeUID TEMPERATURE_CHANNELTYPEUID = DefaultSystemChannelTypeProvider.SYSTEM_OUTDOOR_TEMPERATURE
.getUID();
public static final ChannelTypeUID HUMIDITY_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "humidity");
public static final ChannelTypeUID INDOOR_HUMIDITY_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"indoor-humidity");
public static final ChannelTypeUID DEW_POINT_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "dew-point");
public static final ChannelTypeUID WIND_CHILL_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "wind-chill");
// for extra outdoor sensors use temp2f, temp3f, and so on
public static final ChannelTypeUID INDOOR_TEMPERATURE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"indoor-temperature");
// for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f
public static final ChannelTypeUID SOIL_TEMPERATURE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"soil-temperature");
public static final ChannelTypeUID RAIN_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "rain");
public static final ChannelTypeUID RAIN_DAILY_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "rain-daily");
public static final ChannelTypeUID RAIN_WEEKLY_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "rain-weekly");
public static final ChannelTypeUID RAIN_MONTHLY_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "rain-monthly");
public static final ChannelTypeUID RAIN_YEARLY_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "rain-yearly");
public static final ChannelTypeUID METAR_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "metar");
public static final ChannelTypeUID CLOUDS_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "clouds");
// for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4
public static final ChannelTypeUID SOIL_MOISTURE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "soil-moisture");
// for sensor 2 use leafwetness2
public static final ChannelTypeUID LEAFWETNESS_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "leafwetness");
public static final ChannelTypeUID SOLARRADIATION_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "solarradiation");
public static final ChannelTypeUID UV_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "uv");
public static final ChannelTypeUID VISIBILITY_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "visibility");
public static final ChannelTypeUID BAROMETRIC_PRESSURE_CHANNELTYPEUID = DefaultSystemChannelTypeProvider.SYSTEM_BAROMETRIC_PRESSURE
.getUID();
public static final ChannelTypeUID NITRIC_OXIDE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "nitric-oxide");
public static final ChannelTypeUID NITROGEN_DIOXIDE_MEASURED_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"nitrogen-dioxide-measured");
public static final ChannelTypeUID NITROGEN_DIOXIDE_NOX_NO_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"nitrogen-dioxide-nox-no");
public static final ChannelTypeUID NITROGEN_DIOXIDE_NOY_NO_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"nitrogen-dioxide-noy-no");
public static final ChannelTypeUID NITROGEN_OXIDES_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"nitrogen-oxides");
public static final ChannelTypeUID TOTAL_REACTIVE_NITROGEN_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"total-reactive-nitrogen");
public static final ChannelTypeUID NO3_ION_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "no3-ion");
public static final ChannelTypeUID SO4_ION_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "so4-ion");
public static final ChannelTypeUID SULFUR_DIOXIDE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "sulfur-dioxide");
public static final ChannelTypeUID SULFUR_DIOXIDE_TRACE_LEVELS_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"sulfur-dioxide-trace-levels");
public static final ChannelTypeUID CARBON_MONOXIDE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"carbon-monoxide");
public static final ChannelTypeUID CARBON_MONOXIDE_TRACE_LEVELS_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"carbon-monoxide-trace-levels");
public static final ChannelTypeUID ELEMENTAL_CARBON_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID,
"elemental-carbon");
public static final ChannelTypeUID ORGANIC_CARBON_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "organic-carbon");
public static final ChannelTypeUID BLACK_CARBON_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "black-carbon");
public static final ChannelTypeUID AETHALOMETER_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "aethalometer");
public static final ChannelTypeUID PM2_5_MASS_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "pm2_5-mass");
public static final ChannelTypeUID PM10_MASS_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "pm10-mass");
public static final ChannelTypeUID OZONE_CHANNELTYPEUID = new ChannelTypeUID(BINDING_ID, "ozone");
public static final Unit<Length> NAUTICAL_MILE = addUnit(
new TransformedUnit<>("NM", METRE, MultiplyConverter.of(1852.0)));
static {
SimpleUnitFormat.getInstance().label(NAUTICAL_MILE, "NM");
}
private static <U extends Unit<?>> U addUnit(U unit) {
return unit;
}
}

View File

@ -0,0 +1,30 @@
/**
* 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.wundergroundupdatereceiver.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link WundergroundUpdateReceiverConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault
public class WundergroundUpdateReceiverConfiguration {
/**
* The station id as registered by wunderground.com or any string if not forwarding data to wunderground.com.
* This value will be used as the representation property.
*/
public String stationId = "";
}

View File

@ -0,0 +1,129 @@
/**
* 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.wundergroundupdatereceiver.internal;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResult;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.ThingUID;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Modified;
/**
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault
@Component(service = { DiscoveryService.class,
WundergroundUpdateReceiverDiscoveryService.class }, configurationPid = "discovery.wundergroundupdatereceiver")
public class WundergroundUpdateReceiverDiscoveryService extends AbstractDiscoveryService {
@Nullable
WundergroundUpdateReceiverServletControls servletControls;
private static final int TIMEOUT_SEC = 1;
private final HashMap<String, Map<String, String[]>> thinglessStationIds = new HashMap<>();
private boolean servletWasInactive = false;
private boolean scanning = false;
@Activate
public WundergroundUpdateReceiverDiscoveryService() throws IllegalArgumentException {
this(true);
}
public WundergroundUpdateReceiverDiscoveryService(boolean useBackgroundDiscovery) throws IllegalArgumentException {
super(WundergroundUpdateReceiverBindingConstants.SUPPORTED_THING_TYPES_UIDS, TIMEOUT_SEC,
useBackgroundDiscovery);
}
public void removeUnhandledStationId(String stationId) {
thinglessStationIds.remove(stationId);
}
public void addUnhandledStationId(@Nullable String stationId, Map<String, String[]> request) {
if (stationId == null || stationId.isEmpty()) {
return;
}
if (!this.thinglessStationIds.containsKey(stationId)) {
this.thinglessStationIds.put(stationId, request);
if (isBackgroundDiscoveryEnabled()) {
createDiscoveryResult(stationId);
}
}
}
public boolean isDiscovering() {
return isBackgroundDiscoveryEnabled() || isScanning();
}
public @Nullable Map<String, String[]> getUnhandledStationRequest(@Nullable String stationId) {
return this.thinglessStationIds.get(stationId);
}
private void createDiscoveryResult(String stationId) {
ThingUID id = new ThingUID(WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER, stationId);
DiscoveryResult discoveryResult = DiscoveryResultBuilder.create(id)
.withRepresentationProperty(WundergroundUpdateReceiverBindingConstants.REPRESENTATION_PROPERTY)
.withProperty(WundergroundUpdateReceiverBindingConstants.REPRESENTATION_PROPERTY, stationId)
.withThingType(WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER)
.withLabel("WundergroundUpdateReceiver ID " + stationId).build();
this.thingDiscovered(discoveryResult);
}
@Override
@Activate
protected void activate(@Nullable Map<String, Object> configProperties) {
super.activate(configProperties);
}
@Override
@Modified
protected void modified(@Nullable Map<String, Object> configProperties) {
super.modified(configProperties);
}
@Override
protected void startScan() {
setScanning(true);
if (servletControls != null && !servletControls.isActive()) {
servletWasInactive = true;
servletControls.activate();
}
thinglessStationIds.keySet().forEach(this::createDiscoveryResult);
}
@Override
protected synchronized void stopScan() {
super.stopScan();
thinglessStationIds.keySet().forEach(this::createDiscoveryResult);
if (!isBackgroundDiscoveryEnabled() && servletControls != null && servletWasInactive) {
servletControls.deactivate();
}
setScanning(false);
}
protected synchronized boolean isScanning() {
return this.scanning;
}
protected synchronized void setScanning(boolean value) {
this.scanning = value;
}
}

View File

@ -0,0 +1,214 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.*;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import javax.measure.Quantity;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelGroupUID;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WundergroundUpdateReceiverHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault
public class WundergroundUpdateReceiverHandler extends BaseThingHandler {
public String getStationId() {
return config.stationId;
}
private final Logger logger = LoggerFactory.getLogger(WundergroundUpdateReceiverHandler.class);
private final WundergroundUpdateReceiverServlet wundergroundUpdateReceiverServlet;
private final WundergroundUpdateReceiverDiscoveryService discoveryService;
private final WundergroundUpdateReceiverUnknownChannelTypeProvider channelTypeProvider;
private final ChannelTypeRegistry channelTypeRegistry;
private final ChannelUID dateutcDatetimeChannel;
private final ChannelUID lastReceivedChannel;
private final ChannelUID queryStateChannel;
private final ChannelUID queryTriggerChannel;
private WundergroundUpdateReceiverConfiguration config = new WundergroundUpdateReceiverConfiguration();
public WundergroundUpdateReceiverHandler(Thing thing,
WundergroundUpdateReceiverServlet wunderGroundUpdateReceiverServlet,
WundergroundUpdateReceiverDiscoveryService discoveryService,
WundergroundUpdateReceiverUnknownChannelTypeProvider channelTypeProvider,
ChannelTypeRegistry channelTypeRegistry) {
super(thing);
this.discoveryService = discoveryService;
this.channelTypeProvider = channelTypeProvider;
this.channelTypeRegistry = channelTypeRegistry;
final ChannelGroupUID metadatGroupUID = new ChannelGroupUID(getThing().getUID(), METADATA_GROUP);
this.dateutcDatetimeChannel = new ChannelUID(metadatGroupUID, DATEUTC_DATETIME);
this.lastReceivedChannel = new ChannelUID(metadatGroupUID, LAST_RECEIVED);
this.queryTriggerChannel = new ChannelUID(metadatGroupUID, LAST_QUERY_TRIGGER);
this.queryStateChannel = new ChannelUID(metadatGroupUID, LAST_QUERY_STATE);
this.wundergroundUpdateReceiverServlet = Objects.requireNonNull(wunderGroundUpdateReceiverServlet);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.trace("Ignoring command {}", command);
}
@Override
public void handleConfigurationUpdate(Map<String, Object> configurationParameters) {
super.handleConfigurationUpdate(configurationParameters);
this.wundergroundUpdateReceiverServlet.handlerConfigUpdated(this);
}
@Override
public void initialize() {
this.config = getConfigAs(WundergroundUpdateReceiverConfiguration.class);
wundergroundUpdateReceiverServlet.addHandler(this);
@Nullable
Map<String, String[]> requestParameters = discoveryService.getUnhandledStationRequest(config.stationId);
if (requestParameters != null && thing.getChannels().isEmpty()) {
final String[] noValues = new String[0];
ThingBuilder thingBuilder = editThing();
List.of(LAST_RECEIVED, LAST_QUERY_TRIGGER, DATEUTC_DATETIME, LAST_QUERY_STATE)
.forEach((String channelId) -> buildChannel(thingBuilder, channelId, noValues));
requestParameters
.forEach((String parameter, String[] query) -> buildChannel(thingBuilder, parameter, query));
updateThing(thingBuilder.build());
}
discoveryService.removeUnhandledStationId(config.stationId);
if (wundergroundUpdateReceiverServlet.isActive()) {
updateStatus(ThingStatus.ONLINE);
logger.debug("Wunderground update receiver listening for updates to station id {}", config.stationId);
return;
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR,
wundergroundUpdateReceiverServlet.getErrorDetail());
}
@Override
public void dispose() {
wundergroundUpdateReceiverServlet.removeHandler(this.getStationId());
super.dispose();
logger.debug("Wunderground update receiver stopped listening for updates to station id {}", config.stationId);
}
public void updateChannelStates(Map<String, String> requestParameters) {
requestParameters.forEach(this::updateChannelState);
updateState(lastReceivedChannel, new DateTimeType());
updateState(dateutcDatetimeChannel, safeResolvUtcDateTime(requestParameters.getOrDefault(DATEUTC, NOW)));
String lastQuery = requestParameters.getOrDefault(LAST_QUERY, "");
if (lastQuery.isEmpty()) {
return;
}
updateState(queryStateChannel, StringType.valueOf(lastQuery));
triggerChannel(queryTriggerChannel, lastQuery);
}
private void buildChannel(ThingBuilder thingBuilder, String parameter, String... query) {
@Nullable
WundergroundUpdateReceiverParameterMapping channelTypeMapping = WundergroundUpdateReceiverParameterMapping
.getOrCreateMapping(parameter, String.join("", query), channelTypeProvider);
if (channelTypeMapping == null) {
return;
}
ChannelType channelType = channelTypeRegistry.getChannelType(channelTypeMapping.channelTypeId);
if (channelType == null) {
return;
}
ChannelBuilder channelBuilder = ChannelBuilder
.create(new ChannelUID(thing.getUID(), channelTypeMapping.channelGroup, parameter))
.withType(channelTypeMapping.channelTypeId).withAcceptedItemType(channelType.getItemType());
thingBuilder.withChannel(channelBuilder.build());
}
private DateTimeType safeResolvUtcDateTime(String dateUtc) {
if (!dateUtc.isEmpty() && !NOW.equals(dateUtc)) {
try {
// Supposedly the format is "yyyy-MM-dd hh:mm:ss" from the weather station
return new DateTimeType(ZonedDateTime.parse(dateUtc.replace(" ", "T") + "Z"));
} catch (Exception ex) {
logger.warn("The weather station is submitting unparsable datetime values: {}", dateUtc);
}
}
return new DateTimeType();
}
public void updateChannelState(String channelId, String[] stateParts) {
updateChannelState(channelId, String.join("", stateParts));
}
public void updateChannelState(String parameterName, String state) {
Optional<Channel> channel = getThing().getChannels().stream()
.filter(ch -> parameterName.equals(ch.getUID().getIdWithoutGroup())).findFirst();
if (channel.isPresent()) {
ChannelUID channelUID = channel.get().getUID();
@Nullable
Float numberValue = null;
try {
numberValue = Float.valueOf(state);
} catch (NumberFormatException ignored) {
}
if (numberValue == null) {
updateState(channelUID, StringType.valueOf(state));
return;
}
@Nullable
Unit<? extends Quantity<?>> unit = WundergroundUpdateReceiverParameterMapping.getUnit(parameterName);
if (unit != null) {
updateState(channelUID, new QuantityType<>(numberValue, unit));
} else if (LOW_BATTERY.equals(parameterName)) {
updateState(channelUID, OnOffType.from(state));
} else {
updateState(channelUID, new DecimalType(numberValue));
}
} else if (this.discoveryService.isDiscovering()
&& !WundergroundUpdateReceiverParameterMapping.isExcluded(parameterName)) {
ThingBuilder thingBuilder = editThing();
buildChannel(thingBuilder, parameterName, state);
updateThing(thingBuilder.build());
}
}
}

View File

@ -0,0 +1,85 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.osgi.service.component.ComponentContext;
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 WundergroundUpdateReceiverHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.wundergroundupdatereceiver", service = ThingHandlerFactory.class)
public class WundergroundUpdateReceiverHandlerFactory extends BaseThingHandlerFactory {
private final WundergroundUpdateReceiverDiscoveryService discoveryService;
private final ChannelTypeRegistry channelTypeRegistry;
private final WundergroundUpdateReceiverUnknownChannelTypeProvider channelTypeProvider;
private final WundergroundUpdateReceiverServlet wunderGroundUpdateReceiverServlet;
@Activate
public WundergroundUpdateReceiverHandlerFactory(@Reference HttpService httpService,
@Reference WundergroundUpdateReceiverDiscoveryService discoveryService,
@Reference WundergroundUpdateReceiverUnknownChannelTypeProvider channelTypeProvider,
@Reference ChannelTypeRegistry channelTypeRegistry) {
this.discoveryService = discoveryService;
this.channelTypeRegistry = channelTypeRegistry;
this.channelTypeProvider = channelTypeProvider;
this.wunderGroundUpdateReceiverServlet = new WundergroundUpdateReceiverServlet(httpService,
this.discoveryService);
this.discoveryService.servletControls = this.wunderGroundUpdateReceiverServlet;
if (this.discoveryService.isBackgroundDiscoveryEnabled()) {
this.wunderGroundUpdateReceiverServlet.activate();
}
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return WundergroundUpdateReceiverBindingConstants.SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_UPDATE_RECEIVER.equals(thingTypeUID)) {
return new WundergroundUpdateReceiverHandler(thing, this.wunderGroundUpdateReceiverServlet,
this.discoveryService, this.channelTypeProvider, this.channelTypeRegistry);
}
return null;
}
@Override
protected void deactivate(ComponentContext componentContext) {
this.wunderGroundUpdateReceiverServlet.deactivate();
this.wunderGroundUpdateReceiverServlet.dispose();
super.deactivate(componentContext);
}
}

View File

@ -0,0 +1,198 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.*;
import static org.openhab.core.library.unit.ImperialUnits.*;
import static org.openhab.core.library.unit.Units.*;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import javax.measure.Quantity;
import javax.measure.Unit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.type.ChannelTypeUID;
/**
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault
public class WundergroundUpdateReceiverParameterMapping {
public final String parameterName;
public final ChannelTypeUID channelTypeId;
public final String channelGroup;
public final @Nullable Unit<? extends Quantity<?>> unit;
public final boolean isIndexable;
public final @Nullable Pattern pattern;
private WundergroundUpdateReceiverParameterMapping(String parameterName, ChannelTypeUID channelTypeId,
String channelGroup, @Nullable Unit<? extends Quantity<?>> unit, boolean isIndexable,
@Nullable Pattern pattern) {
this.parameterName = parameterName;
this.channelTypeId = channelTypeId;
this.channelGroup = channelGroup;
this.unit = unit;
this.isIndexable = isIndexable;
this.pattern = pattern;
}
private static final List<String> UNMAPPED_PARAMETERS = List.of(STATION_ID_PARAMETER, PASSWORD, ACTION,
REALTIME_MARKER);
private static final WundergroundUpdateReceiverParameterMapping[] KNOWN_MAPPINGS = {
new WundergroundUpdateReceiverParameterMapping(LAST_RECEIVED, LAST_RECEIVED_DATETIME_CHANNELTYPEUID,
METADATA_GROUP, null, false, null),
new WundergroundUpdateReceiverParameterMapping(LAST_QUERY_STATE, LAST_QUERY_STATE_CHANNELTYPEUID,
METADATA_GROUP, null, false, null),
new WundergroundUpdateReceiverParameterMapping(LAST_QUERY_TRIGGER, LAST_QUERY_TRIGGER_CHANNELTYPEUID,
METADATA_GROUP, null, false, null),
new WundergroundUpdateReceiverParameterMapping(DATEUTC_DATETIME, DATEUTC_DATETIME_CHANNELTYPEUID,
METADATA_GROUP, null, false, null),
new WundergroundUpdateReceiverParameterMapping(DATEUTC, DATEUTC_CHANNELTYPEUID, METADATA_GROUP, null, false,
null),
new WundergroundUpdateReceiverParameterMapping(SOFTWARE_TYPE, SOFTWARETYPE_CHANNELTYPEUID, METADATA_GROUP,
null, false, null),
new WundergroundUpdateReceiverParameterMapping(LOW_BATTERY, LOW_BATTERY_CHANNELTYPEUID, METADATA_GROUP,
null, false, null),
new WundergroundUpdateReceiverParameterMapping(REALTIME_FREQUENCY, REALTIME_FREQUENCY_CHANNELTYPEUID,
METADATA_GROUP, null, false, null),
new WundergroundUpdateReceiverParameterMapping(WIND_DIRECTION, WIND_DIRECTION_CHANNELTYPEUID, WIND_GROUP,
DEGREE_ANGLE, false, null),
new WundergroundUpdateReceiverParameterMapping(WIND_SPEED, WIND_SPEED_CHANNELTYPEUID, WIND_GROUP,
MILES_PER_HOUR, false, null),
new WundergroundUpdateReceiverParameterMapping(GUST_SPEED, GUST_SPEED_CHANNELTYPEUID, WIND_GROUP,
MILES_PER_HOUR, false, null),
new WundergroundUpdateReceiverParameterMapping(GUST_DIRECTION, GUST_DIRECTION_CHANNELTYPEUID, WIND_GROUP,
DEGREE_ANGLE, false, null),
new WundergroundUpdateReceiverParameterMapping(WIND_SPEED_AVG_2MIN, WIND_SPEED_AVG_2MIN_CHANNELTYPEUID,
WIND_GROUP, MILES_PER_HOUR, false, null),
new WundergroundUpdateReceiverParameterMapping(WIND_DIRECTION_AVG_2MIN,
WIND_DIRECTION_AVG_2MIN_CHANNELTYPEUID, WIND_GROUP, DEGREE_ANGLE, false, null),
new WundergroundUpdateReceiverParameterMapping(GUST_SPEED_AVG_10MIN, GUST_SPEED_10MIN_CHANNELTYPEUID,
WIND_GROUP, MILES_PER_HOUR, false, null),
new WundergroundUpdateReceiverParameterMapping(GUST_DIRECTION_AVG_10MIN,
GUST_DIRECTION_10MIN_CHANNELTYPEUID, WIND_GROUP, DEGREE_ANGLE, false, null),
new WundergroundUpdateReceiverParameterMapping(TEMPERATURE, TEMPERATURE_CHANNELTYPEUID, TEMPERATURE_GROUP,
FAHRENHEIT, true, Pattern.compile(TEMPERATURE_INDEXED)),
new WundergroundUpdateReceiverParameterMapping(INDOOR_TEMPERATURE, INDOOR_TEMPERATURE_CHANNELTYPEUID,
TEMPERATURE_GROUP, FAHRENHEIT, false, null),
new WundergroundUpdateReceiverParameterMapping(SOIL_TEMPERATURE, SOIL_TEMPERATURE_CHANNELTYPEUID,
TEMPERATURE_GROUP, FAHRENHEIT, true, Pattern.compile(SOIL_TEMPERATURE_INDEXED)),
new WundergroundUpdateReceiverParameterMapping(WIND_CHILL, WIND_CHILL_CHANNELTYPEUID, TEMPERATURE_GROUP,
FAHRENHEIT, false, null),
new WundergroundUpdateReceiverParameterMapping(HUMIDITY, HUMIDITY_CHANNELTYPEUID, HUMIDITY_GROUP, PERCENT,
false, null),
new WundergroundUpdateReceiverParameterMapping(INDOOR_HUMIDITY, INDOOR_HUMIDITY_CHANNELTYPEUID,
HUMIDITY_GROUP, PERCENT, false, null),
new WundergroundUpdateReceiverParameterMapping(DEWPOINT, DEW_POINT_CHANNELTYPEUID, HUMIDITY_GROUP,
FAHRENHEIT, false, null),
new WundergroundUpdateReceiverParameterMapping(SOIL_MOISTURE, SOIL_MOISTURE_CHANNELTYPEUID, HUMIDITY_GROUP,
PERCENT, true, Pattern.compile(SOIL_MOISTURE_INDEXED)),
new WundergroundUpdateReceiverParameterMapping(LEAF_WETNESS, LEAFWETNESS_CHANNELTYPEUID, HUMIDITY_GROUP,
PERCENT, true, Pattern.compile(LEAF_WETNESS_INDEXED)),
new WundergroundUpdateReceiverParameterMapping(RAIN_IN, RAIN_CHANNELTYPEUID, RAIN_GROUP, INCH, false, null),
new WundergroundUpdateReceiverParameterMapping(DAILY_RAIN_IN, RAIN_DAILY_CHANNELTYPEUID, RAIN_GROUP, INCH,
false, null),
new WundergroundUpdateReceiverParameterMapping(WEEKLY_RAIN_IN, RAIN_WEEKLY_CHANNELTYPEUID, RAIN_GROUP, INCH,
false, null),
new WundergroundUpdateReceiverParameterMapping(MONTHLY_RAIN_IN, RAIN_MONTHLY_CHANNELTYPEUID, RAIN_GROUP,
INCH, false, null),
new WundergroundUpdateReceiverParameterMapping(YEARLY_RAIN_IN, RAIN_YEARLY_CHANNELTYPEUID, RAIN_GROUP, INCH,
false, null),
new WundergroundUpdateReceiverParameterMapping(SOLAR_RADIATION, SOLARRADIATION_CHANNELTYPEUID,
SUNLIGHT_GROUP, IRRADIANCE, false, null),
new WundergroundUpdateReceiverParameterMapping(UV, UV_CHANNELTYPEUID, SUNLIGHT_GROUP, null, false, null),
new WundergroundUpdateReceiverParameterMapping(VISIBILITY, VISIBILITY_CHANNELTYPEUID, SUNLIGHT_GROUP,
NAUTICAL_MILE, false, null),
new WundergroundUpdateReceiverParameterMapping(WEATHER, METAR_CHANNELTYPEUID, SUNLIGHT_GROUP, null, false,
null),
new WundergroundUpdateReceiverParameterMapping(CLOUDS, CLOUDS_CHANNELTYPEUID, SUNLIGHT_GROUP, null, false,
null),
new WundergroundUpdateReceiverParameterMapping(BAROM_IN, BAROMETRIC_PRESSURE_CHANNELTYPEUID, PRESSURE_GROUP,
INCH_OF_MERCURY, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_NO, NITRIC_OXIDE_CHANNELTYPEUID, POLLUTION_GROUP,
PARTS_PER_BILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_NO2, NITROGEN_DIOXIDE_NOX_NO_CHANNELTYPEUID,
POLLUTION_GROUP, PARTS_PER_BILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_NO2T, NITROGEN_DIOXIDE_MEASURED_CHANNELTYPEUID,
POLLUTION_GROUP, PARTS_PER_BILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_NO2Y, NITROGEN_DIOXIDE_NOY_NO_CHANNELTYPEUID,
POLLUTION_GROUP, PARTS_PER_BILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_NOX, NITROGEN_OXIDES_CHANNELTYPEUID, POLLUTION_GROUP,
PARTS_PER_BILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_NOY, TOTAL_REACTIVE_NITROGEN_CHANNELTYPEUID,
POLLUTION_GROUP, PARTS_PER_BILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_NO3, NO3_ION_CHANNELTYPEUID, POLLUTION_GROUP,
MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_SO2, SULFUR_DIOXIDE_CHANNELTYPEUID, POLLUTION_GROUP,
PARTS_PER_BILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_SO2T, SULFUR_DIOXIDE_TRACE_LEVELS_CHANNELTYPEUID,
POLLUTION_GROUP, PARTS_PER_BILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_SO4, SO4_ION_CHANNELTYPEUID, POLLUTION_GROUP,
MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_CO, CARBON_MONOXIDE_CHANNELTYPEUID, POLLUTION_GROUP,
PARTS_PER_MILLION, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_COT, CARBON_MONOXIDE_TRACE_LEVELS_CHANNELTYPEUID,
POLLUTION_GROUP, MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_EC, ELEMENTAL_CARBON_CHANNELTYPEUID, POLLUTION_GROUP,
MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_OC, ORGANIC_CARBON_CHANNELTYPEUID, POLLUTION_GROUP,
MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_BC, BLACK_CARBON_CHANNELTYPEUID, POLLUTION_GROUP,
MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_UV_AETH, AETHALOMETER_CHANNELTYPEUID, POLLUTION_GROUP,
MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_PM2_5, PM2_5_MASS_CHANNELTYPEUID, POLLUTION_GROUP,
MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_PM10, PM10_MASS_CHANNELTYPEUID, POLLUTION_GROUP,
MICROGRAM_PER_CUBICMETRE, false, null),
new WundergroundUpdateReceiverParameterMapping(AQ_OZONE, OZONE_CHANNELTYPEUID, POLLUTION_GROUP,
PARTS_PER_BILLION, false, null) };
public static @Nullable WundergroundUpdateReceiverParameterMapping getOrCreateMapping(String parameterName,
String value, WundergroundUpdateReceiverUnknownChannelTypeProvider channelTypeProvider) {
if (isExcluded(parameterName)) {
return null;
}
Optional<WundergroundUpdateReceiverParameterMapping> knownMapping = lookupMapping(parameterName);
return knownMapping.orElseGet(() -> new WundergroundUpdateReceiverParameterMapping(parameterName,
channelTypeProvider.getOrCreateChannelType(parameterName, value).getUID(), "Uncategorized", null, false,
null));
}
public static boolean isExcluded(String parameter) {
return UNMAPPED_PARAMETERS.contains(parameter);
}
public static @Nullable Unit<? extends Quantity<?>> getUnit(String parameterName) {
Optional<WundergroundUpdateReceiverParameterMapping> mapping = lookupMapping(parameterName);
return mapping
.<Unit<? extends Quantity<?>>> map(
wundergroundUpdateReceiverParameterMapping -> wundergroundUpdateReceiverParameterMapping.unit)
.orElse(null);
}
private static Optional<WundergroundUpdateReceiverParameterMapping> lookupMapping(String parameterName) {
return Arrays.stream(KNOWN_MAPPINGS)
.filter((WundergroundUpdateReceiverParameterMapping m) -> m.parameterName.equals(parameterName)
|| (m.isIndexable && m.pattern != null && m.pattern.matcher(parameterName).matches()))
.findFirst();
}
}

View File

@ -0,0 +1,56 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.profiles.ProfileAdvisor;
import org.openhab.core.thing.profiles.ProfileTypeUID;
import org.openhab.core.thing.profiles.SystemProfiles;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.osgi.service.component.annotations.Component;
/**
* The {@link WundergroundUpdateReceiverProfileAdvisor} suggests the default profile for
* all channels except last updated.
*
* @author Daniel Demus - Initial contribution
*/
@Component
@NonNullByDefault
public class WundergroundUpdateReceiverProfileAdvisor implements ProfileAdvisor {
@Override
public @Nullable ProfileTypeUID getSuggestedProfileTypeUID(Channel channel, @Nullable String itemType) {
return getSuggestedProfileTypeUID(channel.getChannelTypeUID());
}
@Override
public @Nullable ProfileTypeUID getSuggestedProfileTypeUID(ChannelType channelType, @Nullable String itemType) {
return getSuggestedProfileTypeUID(channelType.getUID());
}
private @Nullable ProfileTypeUID getSuggestedProfileTypeUID(@Nullable ChannelTypeUID channelTypeUID) {
if (channelTypeUID == null || !channelTypeUID.getBindingId().equals(BINDING_ID)) {
return null;
}
if (LAST_RECEIVED_DATETIME_CHANNELTYPEUID.equals(channelTypeUID)) {
return SystemProfiles.TIMESTAMP_UPDATE;
}
return SystemProfiles.DEFAULT;
}
}

View File

@ -0,0 +1,219 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.*;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.Instant;
import java.util.Collections;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.io.http.servlet.BaseOpenHABServlet;
import org.osgi.service.http.HttpContext;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link WundergroundUpdateReceiverServlet} is responsible for receiving updates,and
* updating the matching channels.
*
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault
public class WundergroundUpdateReceiverServlet extends BaseOpenHABServlet
implements WundergroundUpdateReceiverServletControls {
public static final String SERVLET_URL = "/weatherstation/updateweatherstation.php";
private static final long serialVersionUID = -5296703727081438023L;
private final Logger logger = LoggerFactory.getLogger(WundergroundUpdateReceiverServlet.class);
private final Map<String, WundergroundUpdateReceiverHandler> handlers = new HashMap<>();
private static final Object LOCK = new Object();
private final WundergroundUpdateReceiverDiscoveryService discoveryService;
private boolean active = false;
private String errorDetail = "";
public WundergroundUpdateReceiverServlet(HttpService httpService,
WundergroundUpdateReceiverDiscoveryService discoveryService) {
super(httpService);
this.discoveryService = discoveryService;
}
public boolean isActive() {
synchronized (LOCK) {
return this.active;
}
}
public String getErrorDetail() {
synchronized (LOCK) {
return this.errorDetail;
}
}
public Set<String> getStationIds() {
return this.handlers.keySet();
}
public void activate() {
activate(SERVLET_URL, httpService.createDefaultHttpContext());
}
public void addHandler(WundergroundUpdateReceiverHandler handler) {
synchronized (this.handlers) {
if (this.handlers.containsKey(handler.getStationId())) {
errorDetail = "Handler handling request for stationId " + handler.getStationId() + " is already added";
logger.warn("Error during handler registration - StationId {} already being handled",
handler.getStationId());
return;
}
this.handlers.put(handler.getStationId(), handler);
errorDetail = "";
if (!isActive()) {
activate();
}
}
}
public void removeHandler(String stationId) {
synchronized (this.handlers) {
WundergroundUpdateReceiverHandler handler = this.handlers.get(stationId);
if (handler != null) {
this.handlers.remove(stationId);
}
if (this.handlers.isEmpty() && !this.discoveryService.isBackgroundDiscoveryEnabled()) {
deactivate();
}
}
}
public void deactivate() {
synchronized (LOCK) {
logger.debug("Stopping servlet {} at {}", getClass().getSimpleName(), SERVLET_URL);
try {
super.deactivate(SERVLET_URL);
} catch (IllegalArgumentException ignored) {
// SERVLET_URL is already unregistered
}
errorDetail = "";
active = false;
}
}
public void handlerConfigUpdated(WundergroundUpdateReceiverHandler handler) {
synchronized (this.handlers) {
final Set<Map.Entry<String, WundergroundUpdateReceiverHandler>> changedStationIds = this.handlers.entrySet()
.stream().filter(entry -> handler.getThing().getUID().equals(entry.getValue().getThing().getUID()))
.collect(toSet());
changedStationIds.forEach(entry -> {
logger.debug("Re-assigning listener from station id {} to station id {}", entry.getKey(),
handler.getStationId());
this.removeHandler(entry.getKey());
this.addHandler(handler);
});
}
}
public void dispose() {
synchronized (this.handlers) {
Set<String> stationIds = new HashSet<>(getStationIds());
stationIds.forEach(this::removeHandler);
deactivate();
}
}
@Override
protected void activate(String alias, HttpContext httpContext) {
synchronized (LOCK) {
try {
logger.debug("Starting servlet {} at {}", getClass().getSimpleName(), alias);
Dictionary<String, String> props = new Hashtable<>(1, 10);
httpService.registerServlet(alias, this, props, httpContext);
errorDetail = "";
active = true;
} catch (NamespaceException e) {
active = false;
errorDetail = "Servlet couldn't be registered - alias " + alias + " already in use";
logger.warn("Error during servlet registration - alias {} already in use", alias, e);
} catch (ServletException e) {
active = false;
errorDetail = "Servlet couldn't be registered - " + e.getMessage();
logger.warn("Error during servlet registration", e);
}
}
}
@Override
protected void doGet(@Nullable HttpServletRequest req, @Nullable HttpServletResponse resp) throws IOException {
if (!active) {
return;
}
if (req == null) {
return;
}
if (resp == null) {
return;
}
if (req.getRequestURI() == null) {
return;
}
logger.trace("doGet {}", req.getQueryString());
String stationId = req.getParameter(STATION_ID_PARAMETER);
Optional.ofNullable(this.handlers.get(stationId)).ifPresentOrElse(handler -> {
Map<String, String> states = req.getParameterMap().entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> String.join("", e.getValue())));
String queryString = req.getQueryString();
if (queryString != null && queryString.length() > 0) {
states.put(LAST_QUERY, queryString);
}
handler.updateChannelStates(states);
}, () -> {
this.discoveryService.addUnhandledStationId(stationId, req.getParameterMap());
});
resp.setStatus(HttpServletResponse.SC_OK);
resp.setContentType("text/html;charset=utf-8");
resp.setContentLength(7);
resp.setDateHeader("Date", Instant.now().toEpochMilli());
resp.setHeader("Connection", "close");
PrintWriter writer = resp.getWriter();
writer.write("success");
writer.flush();
writer.close();
}
protected Map<String, WundergroundUpdateReceiverHandler> getHandlers() {
return Collections.unmodifiableMap(this.handlers);
}
}

View File

@ -0,0 +1,28 @@
/**
* 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.wundergroundupdatereceiver.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault
public interface WundergroundUpdateReceiverServletControls {
void activate();
void deactivate();
boolean isActive();
}

View File

@ -0,0 +1,83 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.type.ChannelType;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Daniel Demus - Initial contribution
*/
@Component(service = { ChannelTypeProvider.class, WundergroundUpdateReceiverUnknownChannelTypeProvider.class })
@NonNullByDefault
public class WundergroundUpdateReceiverUnknownChannelTypeProvider implements ChannelTypeProvider {
private static final List<String> BOOLEAN_STRINGS = List.of("1", "0", "true", "false");
private final Map<ChannelTypeUID, ChannelType> channelTypes = new ConcurrentHashMap<>();
private final Logger logger = LoggerFactory.getLogger(WundergroundUpdateReceiverUnknownChannelTypeProvider.class);
@Override
public Collection<ChannelType> getChannelTypes(@Nullable Locale locale) {
return channelTypes.values();
}
@Override
public @Nullable ChannelType getChannelType(ChannelTypeUID channelTypeUID, @Nullable Locale locale) {
return channelTypes.get(channelTypeUID);
}
public ChannelType getOrCreateChannelType(String parameterName, String value) {
ChannelTypeUID typeUid = new ChannelTypeUID(THING_TYPE_UPDATE_RECEIVER.getBindingId(), parameterName);
@Nullable
ChannelType type = getChannelType(typeUid, null);
if (type == null) {
String itemType = guessItemType(value);
type = ChannelTypeBuilder.state(typeUid, parameterName + " channel type", itemType).build();
return addChannelType(typeUid, type);
}
return type;
}
private static String guessItemType(String value) {
if (BOOLEAN_STRINGS.contains(value.toLowerCase())) {
return "Switch";
}
try {
Float.valueOf(value);
return "Number";
} catch (NumberFormatException ignored) {
}
return "String";
}
private ChannelType addChannelType(ChannelTypeUID channelTypeUID, ChannelType channelType) {
logger.warn("Adding channelType {} for unknown parameter", channelTypeUID.getAsString());
this.channelTypes.put(channelTypeUID, channelType);
return channelType;
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="wundergroundupdatereceiver" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>Wunderground Update Receiver Binding</name>
<description>
This binding enables acting as a receiver of updates from weather stations that post measurements to
https://rtupdate.wunderground.com/weatherstation/updateweatherstation.php.
</description>
</binding:binding>

View File

@ -0,0 +1,140 @@
# binding
binding.wundergroundupdatereceiver.name = Wunderground Update Receiver Binding
binding.wundergroundupdatereceiver.description = This binding enables acting as a receiver of updates from weather stations that post measurements to https://rtupdate.wunderground.com/weatherstation/updateweatherstation.php.
# thing types
thing-type.wundergroundupdatereceiver.wundergroundUpdateReceiver.label = Update Receiver
thing-type.wundergroundupdatereceiver.wundergroundUpdateReceiver.description = An endpoint thing that can receive and propagate HTTP GET updates meant for a particular station id at wunderground.com
# thing types config
thing-type.config.wundergroundupdatereceiver.wundergroundUpdateReceiver.stationId.label = Station ID
thing-type.config.wundergroundupdatereceiver.wundergroundUpdateReceiver.stationId.description = <br /> The wunderground.com update api requires a station id, that is defined for the WeatherUnderground account measurements are to be submitted to.<br /> <br /> In this binding it is used to identify a unique thing, so each weather-station or other apparatus submitting measurements can have a separate id, but if you don't intend to forward the observations to wunderground.com, this value can be any non-blank string.
# channel group types
channel-group-type.wundergroundupdatereceiver.humidity-group.label = Moisture
channel-group-type.wundergroundupdatereceiver.humidity-group.description = Moisture measurements
channel-group-type.wundergroundupdatereceiver.metadata-group.label = Metadata
channel-group-type.wundergroundupdatereceiver.metadata-group.description = Metadata regarding the last submission
channel-group-type.wundergroundupdatereceiver.pollution-group.label = Pollution
channel-group-type.wundergroundupdatereceiver.pollution-group.description = Pollutant measurements
channel-group-type.wundergroundupdatereceiver.pressure-group.label = Pressure
channel-group-type.wundergroundupdatereceiver.pressure-group.description = Barometric measurements
channel-group-type.wundergroundupdatereceiver.rain-group.label = Rain
channel-group-type.wundergroundupdatereceiver.rain-group.description = Rain measurements
channel-group-type.wundergroundupdatereceiver.sunlight-group.label = Sunlight
channel-group-type.wundergroundupdatereceiver.sunlight-group.description = Sunlight measurements
channel-group-type.wundergroundupdatereceiver.temperature-group.label = Temperature
channel-group-type.wundergroundupdatereceiver.temperature-group.description = Temperature measurements
channel-group-type.wundergroundupdatereceiver.wind-group.label = Wind
channel-group-type.wundergroundupdatereceiver.wind-group.description = Wind measurements
# channel types
channel-type.wundergroundupdatereceiver.aethalometer.label = Second Channel of Aethalometer
channel-type.wundergroundupdatereceiver.aethalometer.description = second channel of Aethalometer at 370 nm, µG/m3.
channel-type.wundergroundupdatereceiver.black-carbon.label = Black Carbon
channel-type.wundergroundupdatereceiver.black-carbon.description = Black Carbon at 880 nm, µG/m3.
channel-type.wundergroundupdatereceiver.carbon-monoxide-trace-levels.label = Carbon Monoxide Trace Levels
channel-type.wundergroundupdatereceiver.carbon-monoxide-trace-levels.description = Carbon Monoxide, trace levels ppb.
channel-type.wundergroundupdatereceiver.carbon-monoxide.label = Carbon Monoxide
channel-type.wundergroundupdatereceiver.carbon-monoxide.description = Carbon Monoxide, conventional ppm.
channel-type.wundergroundupdatereceiver.clouds.label = Cloud Cover
channel-type.wundergroundupdatereceiver.clouds.description = METAR style cloud cover.
channel-type.wundergroundupdatereceiver.dateutc-datetime.label = Last Updated as DateTime
channel-type.wundergroundupdatereceiver.dateutc-datetime.description = The date and time of the last update in UTC as submitted by the weather station converted to a DateTime value. In case of 'now', the current time is used.
channel-type.wundergroundupdatereceiver.dateutc-datetime.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS
channel-type.wundergroundupdatereceiver.dateutc.label = Last Updated
channel-type.wundergroundupdatereceiver.dateutc.description = The date and time of the last update in UTC as submitted by the weather station. This can be 'now'.
channel-type.wundergroundupdatereceiver.dew-point.label = Dew Point
channel-type.wundergroundupdatereceiver.dew-point.description = Outdoor dew point.
channel-type.wundergroundupdatereceiver.elemental-carbon.label = Elemental Carbon
channel-type.wundergroundupdatereceiver.elemental-carbon.description = Elemental Carbon, PM2.5 µG/m3.
channel-type.wundergroundupdatereceiver.humidity.label = Humidity
channel-type.wundergroundupdatereceiver.humidity.description = Humidity in %.
channel-type.wundergroundupdatereceiver.indoor-humidity.label = Indoor Humidity
channel-type.wundergroundupdatereceiver.indoor-humidity.description = Indoor humidity in %.
channel-type.wundergroundupdatereceiver.indoor-temperature.label = Indoor Temperature
channel-type.wundergroundupdatereceiver.indoor-temperature.description = Indoor temperature.
channel-type.wundergroundupdatereceiver.last-query-state.label = The last query
channel-type.wundergroundupdatereceiver.last-query-state.description = The part of the last query after the first unurlencoded '?'
channel-type.wundergroundupdatereceiver.last-query-trigger.label = The last query
channel-type.wundergroundupdatereceiver.last-query-trigger.description = The part of the last query after the first unurlencoded '?'
channel-type.wundergroundupdatereceiver.last-received-datetime.label = Last Received
channel-type.wundergroundupdatereceiver.last-received-datetime.description = The date and time of the last update.
channel-type.wundergroundupdatereceiver.last-received-datetime.state.pattern = %1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS
channel-type.wundergroundupdatereceiver.leafwetness.label = Leaf Wetness
channel-type.wundergroundupdatereceiver.leafwetness.description = Leaf wetness in %.
channel-type.wundergroundupdatereceiver.metar.label = METAR Weather Report
channel-type.wundergroundupdatereceiver.metar.description = METAR formatted weather report
channel-type.wundergroundupdatereceiver.nitric-oxide.label = Nitric Oxide
channel-type.wundergroundupdatereceiver.nitric-oxide.description = Nitric Oxide ppm.
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-measured.label = Nitrogen Dioxide
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-measured.description = Nitrogen Dioxide, true measure ppb.
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-nox-no.label = NO2 X computed
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-nox-no.description = NO2 computed, NOx-NO ppb.
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-noy-no.label = NO2 Y computed, NOy-NO ppb
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-noy-no.description = NO2 computed, NOy-NO ppb.
channel-type.wundergroundupdatereceiver.nitrogen-oxides.label = Nitrogen Oxides
channel-type.wundergroundupdatereceiver.nitrogen-oxides.description = Nitrogen Oxides ppb.
channel-type.wundergroundupdatereceiver.no3-ion.label = NO3 ion
channel-type.wundergroundupdatereceiver.no3-ion.description = NO3 ion (nitrate, not adjusted for ammonium ion) µG/m3.
channel-type.wundergroundupdatereceiver.organic-carbon.label = Organic Carbon
channel-type.wundergroundupdatereceiver.organic-carbon.description = Organic Carbon, not adjusted for oxygen and hydrogen, PM2.5 µG/m3.
channel-type.wundergroundupdatereceiver.ozone.label = Ozone
channel-type.wundergroundupdatereceiver.ozone.description = Ozone, ppb.
channel-type.wundergroundupdatereceiver.pm10-mass.label = PM10 Mass
channel-type.wundergroundupdatereceiver.pm10-mass.description = PM10 mass, µG/m3.
channel-type.wundergroundupdatereceiver.pm2_5-mass.label = PM2.5 Mass
channel-type.wundergroundupdatereceiver.pm2_5-mass.description = PM2.5 mass, µG/m3.
channel-type.wundergroundupdatereceiver.rain-daily.label = Daily Rain
channel-type.wundergroundupdatereceiver.rain-daily.description = Rain since the start of the day.
channel-type.wundergroundupdatereceiver.rain-monthly.label = Monthly Rain
channel-type.wundergroundupdatereceiver.rain-monthly.description = Rain since the start if this month.
channel-type.wundergroundupdatereceiver.rain-weekly.label = Weekly Rain
channel-type.wundergroundupdatereceiver.rain-weekly.description = Rain since the start of this week.
channel-type.wundergroundupdatereceiver.rain-yearly.label = Yearly Rain
channel-type.wundergroundupdatereceiver.rain-yearly.description = Rain since the start of this year.
channel-type.wundergroundupdatereceiver.rain.label = Hourly Rain
channel-type.wundergroundupdatereceiver.rain.description = Rain over the past hour.
channel-type.wundergroundupdatereceiver.realtime-frequency.label = Realtime Frequency
channel-type.wundergroundupdatereceiver.realtime-frequency.description = How often does the weather station submit measurements
channel-type.wundergroundupdatereceiver.so4-ion.label = SO4 ion
channel-type.wundergroundupdatereceiver.so4-ion.description = SO4 ion (sulfate, not adjusted for ammonium ion) µG/m3.
channel-type.wundergroundupdatereceiver.softwaretype.label = Software Type
channel-type.wundergroundupdatereceiver.softwaretype.description = A software type string from the weather station
channel-type.wundergroundupdatereceiver.soil-moisture.label = Soil Moisture
channel-type.wundergroundupdatereceiver.soil-moisture.description = Soil moisture in %.
channel-type.wundergroundupdatereceiver.soil-temperature.label = Soil Temperature
channel-type.wundergroundupdatereceiver.soil-temperature.description = Soil temperature.
channel-type.wundergroundupdatereceiver.solarradiation.label = Solar Radiation
channel-type.wundergroundupdatereceiver.solarradiation.description = Solar radiation
channel-type.wundergroundupdatereceiver.sulfur-dioxide-trace-levels.label = Sulfur Dioxide Trace Levels
channel-type.wundergroundupdatereceiver.sulfur-dioxide-trace-levels.description = Sulfur Dioxide, trace levels ppb.
channel-type.wundergroundupdatereceiver.sulfur-dioxide.label = Sulfur Dioxide
channel-type.wundergroundupdatereceiver.sulfur-dioxide.description = Sulfur Dioxide, conventional ppb.
channel-type.wundergroundupdatereceiver.total-reactive-nitrogen.label = Total Reactive Nitrogen
channel-type.wundergroundupdatereceiver.total-reactive-nitrogen.description = Total reactive nitrogen.
channel-type.wundergroundupdatereceiver.uv.label = UV Index
channel-type.wundergroundupdatereceiver.uv.description = UV index.
channel-type.wundergroundupdatereceiver.visibility.label = Visibility
channel-type.wundergroundupdatereceiver.visibility.description = Visibility.
channel-type.wundergroundupdatereceiver.wind-chill.label = Wind Chill
channel-type.wundergroundupdatereceiver.wind-chill.description = The apparent wind chill temperature.
channel-type.wundergroundupdatereceiver.wind-direction-avg-2min.label = Wind Direction 2min Average
channel-type.wundergroundupdatereceiver.wind-direction-avg-2min.description = 2 minute average wind direction.
channel-type.wundergroundupdatereceiver.wind-gust-direction-10min.label = Gust Direction 10min Average
channel-type.wundergroundupdatereceiver.wind-gust-direction-10min.description = 10 minute average gust direction.
channel-type.wundergroundupdatereceiver.wind-gust-direction.label = Gust Direction
channel-type.wundergroundupdatereceiver.wind-gust-direction.description = Current wind gust direction expressed as an angle using software specific time period.
channel-type.wundergroundupdatereceiver.wind-gust-speed-10min.label = Gust Speed 10min Average
channel-type.wundergroundupdatereceiver.wind-gust-speed-10min.description = 10 minute average gust speed.
channel-type.wundergroundupdatereceiver.wind-gust-speed.label = Current Gust Speed
channel-type.wundergroundupdatereceiver.wind-gust-speed.description = Current wind gust speed, using software specific time period.
channel-type.wundergroundupdatereceiver.wind-speed-avg-2min.label = Wind Speed 2min Average
channel-type.wundergroundupdatereceiver.wind-speed-avg-2min.description = 2 minute average wind speed.
channel-type.wundergroundupdatereceiver.wind-speed.label = Current Wind Speed
channel-type.wundergroundupdatereceiver.wind-speed.description = Current wind speed, using software specific time period.

View File

@ -0,0 +1,129 @@
binding.wundergroundupdatereceiver.name=Wundergroundopdateringsmodtager Binding
binding.wundergroundupdatereceiver.description=Denne binding gør det muligt at modtage opdateringer fra vejrstationer som sender målinger til https://rtupdate.wunderground.com/weatherstation/updateweatherstation.php.
thing-type.wundergroundupdatereceiver.description=En endpoint til modtagelse og videreformidling af opdateringer fra vejrstationer, som kan opdatere wunderground.com
thing-type.wundergroundupdatereceiver.config.label=Stationsid som er konfigureret i vejstationen.
thing-type.wundergroundupdatereceiver.config.description=<![CDATA[<br /> \
wunderground.com api'en kræver en stationsid, som stammer fra WeatherUnderground \
kontoen man sender målinger til.<br /> \
<br /> \
Her bliver den brugt til at skelne mellem ting instanser, så flere vejrstationers opdateringer kan håndteres \
eller flere slags måleenheders målinger kan aggregeres. \
Hvis man ikke agter at videresende målinger til wunderground.com, kan man sætte dette til en ikke-tom streng.
channel-group-type.wundergroundupdatereceiver.metadata-group.description=Metadata fra den sidste opdatering
channel-group-type.wundergroundupdatereceiver.wind-group.description=Vindmålinger
channel-group-type.wundergroundupdatereceiver.wind-group.label=Vind
channel-group-type.wundergroundupdatereceiver.temperature-group.description=Temperaturmålinger
channel-group-type.wundergroundupdatereceiver.temperature-group.label=Temperatur
channel-group-type.wundergroundupdatereceiver.humidity-group.description=Fugtighedsmålinger
channel-group-type.wundergroundupdatereceiver.humidity-group.label=Fugtighed
channel-group-type.wundergroundupdatereceiver.rain-group.description=Regnmålinger
channel-group-type.wundergroundupdatereceiver.rain-group.label=Regn
channel-group-type.wundergroundupdatereceiver.sunlight-group.description=Lysmålinger
channel-group-type.wundergroundupdatereceiver.sunlight-group.label=Sollys
channel-group-type.wundergroundupdatereceiver.pressure-group.description=Barometertrykmålinger
channel-group-type.wundergroundupdatereceiver.pressure-group.label=Barometertryk
channel-group-type.wundergroundupdatereceiver.pollution-group.description=Målinger af atmosfærisk forurening
channel-group-type.wundergroundupdatereceiver.pollution-group.label=Forurening
channel-type.wundergroundupdatereceiver.last-received-datetime.description=Tidspunkt for sidst modtaget opdatering fra stationen
channel-type.wundergroundupdatereceiver.last-received-datetime.label=Sidst modtaget
channel-type.wundergroundupdatereceiver.dateutc.description=Stationens værdi for sidst opdateringstidspunkt. Kan være 'now'
channel-type.wundergroundupdatereceiver.dateutc.label=Sidst opdateret
channel-type.system.low-battery.description=Stationens batteri er ved være flad
channel-type.system.low-battery.label=Lav batteri
channel-type.wundergroundupdatereceiver.realtime-frequency.description=Stationens angivelse af opdateringsfrekvensen
channel-type.wundergroundupdatereceiver.realtime-frequency.label=Opdateringsfrekvens
channel-type.wundergroundupdatereceiver.wind-speed.description=Øjeblikkelig vindhastighed i mph
channel-type.wundergroundupdatereceiver.wind-speed.label=Vindhastighed
channel-type.wundergroundupdatereceiver.wind-gust-speed.description=Øjeblikkelig vindstødshastighed
channel-type.wundergroundupdatereceiver.wind-gust-speed.label=Vindstødshastighed
channel-type.system.wind-direction.description=Øjeblikkelig vindretning som vinkel
channel-type.system.wind-direction.label=Vindretning
channel-type.wundergroundupdatereceiver.wind-speed-avg-2min.description=Gennemsnitlig vindhastighed i de sidste 2 minutter
channel-type.wundergroundupdatereceiver.wind-speed-avg-2min.label=Gns. vindhastighed (2min)
channel-type.wundergroundupdatereceiver.wind-direction-avg-2min.description=Gennemsnitlig vindretning i de sidste 2 minutter som vinkel
channel-type.wundergroundupdatereceiver.wind-direction-avg-2min.label=Gns. vindretning (2min)
channel-type.wundergroundupdatereceiver.wind-gust-speed-10min.description=Gennemsnitlig vindstæshastighed i de sidste 10 minutter
channel-type.wundergroundupdatereceiver.wind-gust-speed-10min.label=Gns. vindstødshastighed (10min)
channel-type.wundergroundupdatereceiver.wind-gust-direction-10min.description=Gennemsnitlig vindstødsretning i de sidste 10 minutter som vinkel
channel-type.wundergroundupdatereceiver.wind-gust-direction-10min.label=Gns. vindstødsretning (10min)
channel-type.wundergroundupdatereceiver.humidity.description=Udendørs fugtighed i pct
channel-type.wundergroundupdatereceiver.humidity.label=Udendørs fugtighed
channel-type.wundergroundupdatereceiver.indoor-humidity.description=Indendørs fugtighed i pct
channel-type.wundergroundupdatereceiver.indoor-humidity.label=Indendørs fugtighed
channel-type.wundergroundupdatereceiver.dew-point.description=Dugpunkt
channel-type.wundergroundupdatereceiver.dew-point.label=Dugpunkt
channel-type.wundergroundupdatereceiver.wind-chill.description=Kuldeindeks
channel-type.wundergroundupdatereceiver.wind-chill.label=Kuldeindeks
channel-type.system.temperature.description=Udendørs temperatur
channel-type.system.temperature.label=Udendørs temperatur
channel-type.wundergroundupdatereceiver.indoor-temperature.description=Indendørs temperatur
channel-type.wundergroundupdatereceiver.indoor-temperature.label=Indendørs temperatur
channel-type.wundergroundupdatereceiver.soil-temperature.description=Jordtemeperatur
channel-type.wundergroundupdatereceiver.soil-temperature.label=Jordtemeperatur
channel-type.wundergroundupdatereceiver.rain.description=Regnmængde i den sidste time
channel-type.wundergroundupdatereceiver.rain.label=Regn (sidste time)
channel-type.wundergroundupdatereceiver.rain-daily.description=Regnmængde siden starten af dagen
channel-type.wundergroundupdatereceiver.rain-daily.label=Regn (i dag)
channel-type.wundergroundupdatereceiver.rain-weekly.description=Regnmængde siden starten af ugen
channel-type.wundergroundupdatereceiver.rain-weekly.label=Regn (denne uge)
channel-type.wundergroundupdatereceiver.rain-monthly.description=Regnmængde siden starten af måneden
channel-type.wundergroundupdatereceiver.rain-monthly.label=Regn (denne måned)
channel-type.wundergroundupdatereceiver.rain-yearly.description=Regnmængde siden starten af året
channel-type.wundergroundupdatereceiver.rain-yearly.label=Regn (i år)
channel-type.wundergroundupdatereceiver.metar.description=Vejrmelding i METAR format
channel-type.wundergroundupdatereceiver.metar.label=METAR vejrmelding
channel-type.wundergroundupdatereceiver.clouds.description=Skydække
channel-type.wundergroundupdatereceiver.clouds.label=Skydække i METAR format
channel-type.wundergroundupdatereceiver.soil-moisture.description=Jordfugtighed i pct
channel-type.wundergroundupdatereceiver.soil-moisture.label=Jordfugtighed
channel-type.wundergroundupdatereceiver.leafwetness.description=Bladfugtighed i pct
channel-type.wundergroundupdatereceiver.leafwetness.label=Bladfugtighed
channel-type.wundergroundupdatereceiver.solarradiation.description=Solstråling i W/m2
channel-type.wundergroundupdatereceiver.solarradiation.label=Solstråling
channel-type.wundergroundupdatereceiver.uv.description=UV index
channel-type.wundergroundupdatereceiver.uv.label=UV index
channel-type.wundergroundupdatereceiver.visibility.description=Sigtbarhed
channel-type.wundergroundupdatereceiver.visibility.label=Sigtbarhed
channel-type.wundergroundupdatereceiver.nitric-oxide.description=
channel-type.wundergroundupdatereceiver.nitric-oxide.label=Nitrogenoxid
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-measured.description=Målt nitrogendioxid i ppb
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-measured.label=Nitrogendioxid
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-nox-no.description=Beregnet nitrogendioxid i ppb
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-nox-no.label=Beregnet nitrogendioxid
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-noy-no.description=Beregnet nitrogendioxid Y
channel-type.wundergroundupdatereceiver.nitrogen-dioxide-noy-no.label=Beregnet nitrogendioxid Y
channel-type.wundergroundupdatereceiver.nitrogen-oxides.description=Nitrogenoxider
channel-type.wundergroundupdatereceiver.nitrogen-oxides.label=Nitrogenoxid i ppb
channel-type.wundergroundupdatereceiver.total-reactive-nitrogen.description=Reactive nitrogen ialt
channel-type.wundergroundupdatereceiver.total-reactive-nitrogen.label=Reactive nitrogen ialt
channel-type.wundergroundupdatereceiver.no3-ion.description=Nitrat tæthed
channel-type.wundergroundupdatereceiver.no3-ion.label=Nitrat
channel-type.wundergroundupdatereceiver.so4-ion.description=Sulfat tæthed
channel-type.wundergroundupdatereceiver.so4-ion.label=Sulfat
channel-type.wundergroundupdatereceiver.sulfur-dioxide.description=Svovldioxid i ppb
channel-type.wundergroundupdatereceiver.sulfur-dioxide.label=Svovldioxid
channel-type.wundergroundupdatereceiver.sulfur-dioxide-trace-levels.description=Svovldioxid sporelementer
channel-type.wundergroundupdatereceiver.sulfur-dioxide-trace-levels.label=Svovldioxid sporelementer
channel-type.wundergroundupdatereceiver.elemental-carbon.description=Kulstofselementer
channel-type.wundergroundupdatereceiver.elemental-carbon.label=Kulstofselementer
channel-type.wundergroundupdatereceiver.organic-carbon.description=Organisk kulstof
channel-type.wundergroundupdatereceiver.organic-carbon.label=Organisk kulstof
channel-type.wundergroundupdatereceiver.black-carbon.description=Sort kulstof
channel-type.wundergroundupdatereceiver.black-carbon.label=Sort kulstof
channel-type.wundergroundupdatereceiver.aethalometer.description=Aethalometer
channel-type.wundergroundupdatereceiver.aethalometer.label=Aethalometer
channel-type.wundergroundupdatereceiver.pm2_5-mass.description=PM2.5 massetæthed
channel-type.wundergroundupdatereceiver.pm2_5-mass.label=PM2.5 massetæthed
channel-type.wundergroundupdatereceiver.pm10-mass.description=PM10 massetæthed
channel-type.wundergroundupdatereceiver.pm10-mass.label=PM10 massetæthed
channel-type.wundergroundupdatereceiver.ozone.description=Ozon
channel-type.wundergroundupdatereceiver.ozone.label=Ozon
channel-type.wundergroundupdatereceiver.softwaretype.description=En software type streng fra vejrstationen
channel-type.wundergroundupdatereceiver.softwaretype.label=Software type
channel-type.wundergroundupdatereceiver.last-query-state.description=Querystring delen af den sidste query
channel-type.wundergroundupdatereceiver.last-query-state.label=Den sidste query
channel-type.wundergroundupdatereceiver.last-query-trigger.description=Querystring delen af den sidste query
channel-type.wundergroundupdatereceiver.last-query-trigger.label=Den sidste query

View File

@ -0,0 +1,54 @@
<?xml version="1.0" ?>
<thing:thing-descriptions bindingId="wundergroundupdatereceiver"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-group-type id="metadata-group">
<label>Metadata</label>
<description>Metadata regarding the last submission</description>
<category>Text</category>
</channel-group-type>
<channel-group-type id="wind-group">
<label>Wind</label>
<description>Wind measurements</description>
<category>Wind</category>
</channel-group-type>
<channel-group-type id="temperature-group">
<label>Temperature</label>
<description>Temperature measurements</description>
<category>Temperature</category>
</channel-group-type>
<channel-group-type id="humidity-group">
<label>Moisture</label>
<description>Moisture measurements</description>
<category>Humidity</category>
</channel-group-type>
<channel-group-type id="rain-group">
<label>Rain</label>
<description>Rain measurements</description>
<category>Rain</category>
</channel-group-type>
<channel-group-type id="sunlight-group">
<label>Sunlight</label>
<description>Sunlight measurements</description>
<category>Sun_Clouds</category>
</channel-group-type>
<channel-group-type id="pressure-group">
<label>Pressure</label>
<description>Barometric measurements</description>
<category>Pressure</category>
</channel-group-type>
<channel-group-type id="pollution-group">
<label>Pollution</label>
<description>Pollutant measurements</description>
<category>Pollution</category>
</channel-group-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,580 @@
<?xml version="1.0" ?>
<thing:thing-descriptions bindingId="wundergroundupdatereceiver"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="last-received-datetime">
<item-type>DateTime</item-type>
<label>Last Received</label>
<description>The date and time of the last update.</description>
<category>Time</category>
<tags>
<tag>Point</tag>
<tag>Timestamp</tag>
</tags>
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
</channel-type>
<channel-type id="dateutc" advanced="true">
<item-type>String</item-type>
<label>Last Updated</label>
<description>The date and time of the last update in UTC as submitted by the weather station. This can be 'now'.</description>
<category>Time</category>
<tags>
<tag>Point</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="dateutc-datetime" advanced="true">
<item-type>DateTime</item-type>
<label>Last Updated as DateTime</label>
<description>The date and time of the last update in UTC as submitted by the weather station converted to a DateTime
value. In case of 'now', the current time is used.</description>
<category>Time</category>
<tags>
<tag>Point</tag>
<tag>Timestamp</tag>
</tags>
<state readOnly="true" pattern="%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS"/>
</channel-type>
<channel-type id="wind-speed">
<item-type>Number:Speed</item-type>
<label>Current Wind Speed</label>
<description>Current wind speed, using software specific time period.</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="wind-gust-speed">
<item-type>Number:Speed</item-type>
<label>Current Gust Speed</label>
<description>Current wind gust speed, using software specific time period.</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="wind-gust-direction">
<item-type>Number:Angle</item-type>
<label>Gust Direction</label>
<description>Current wind gust direction expressed as an angle using software specific time period.</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="wind-speed-avg-2min" advanced="true">
<item-type>Number:Speed</item-type>
<label>Wind Speed 2min Average</label>
<description>2 minute average wind speed.</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="wind-direction-avg-2min" advanced="true">
<item-type>Number:Angle</item-type>
<label>Wind Direction 2min Average</label>
<description>2 minute average wind direction.</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="wind-gust-speed-10min" advanced="true">
<item-type>Number:Speed</item-type>
<label>Gust Speed 10min Average</label>
<description>10 minute average gust speed.</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="wind-gust-direction-10min" advanced="true">
<item-type>Number:Angle</item-type>
<label>Gust Direction 10min Average</label>
<description>10 minute average gust direction.</description>
<category>Wind</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="humidity">
<item-type>Number:Dimensionless</item-type>
<label>Humidity</label>
<description>Humidity in %.</description>
<category>Humidity</category>
<tags>
<tag>Measurement</tag>
<tag>Humidity</tag>
</tags>
<state readOnly="true" pattern="%.0f %%"/>
</channel-type>
<channel-type id="indoor-humidity">
<item-type>Number:Dimensionless</item-type>
<label>Indoor Humidity</label>
<description>Indoor humidity in %.</description>
<category>Humidity</category>
<tags>
<tag>Measurement</tag>
<tag>Humidity</tag>
</tags>
<state readOnly="true" pattern="%.0f %%"/>
</channel-type>
<channel-type id="dew-point" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Dew Point</label>
<description>Outdoor dew point.</description>
<category>Humidity</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="wind-chill" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Wind Chill</label>
<description>The apparent wind chill temperature.</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Wind</tag>
</tags>
<state readOnly="true" pattern="%.1f %unit%"/>
</channel-type>
<!-- for extra outdoor sensors use temp2f, temp3f, and so on -->
<channel-type id="indoor-temperature">
<item-type>Number:Temperature</item-type>
<label>Indoor Temperature</label>
<description>Indoor temperature.</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<!-- for sensors 2,3,4 use soiltemp2f, soiltemp3f, and soiltemp4f -->
<channel-type id="soil-temperature" advanced="true">
<item-type>Number:Temperature</item-type>
<label>Soil Temperature</label>
<description>Soil temperature.</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="rain">
<item-type>Number:Length</item-type>
<label>Hourly Rain</label>
<description>Rain over the past hour.</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="rain-daily">
<item-type>Number:Length</item-type>
<label>Daily Rain</label>
<description>Rain since the start of the day.</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="rain-weekly" advanced="true">
<item-type>Number:Length</item-type>
<label>Weekly Rain</label>
<description>Rain since the start of this week.</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="rain-monthly" advanced="true">
<item-type>Number:Length</item-type>
<label>Monthly Rain</label>
<description>Rain since the start if this month.</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="rain-yearly" advanced="true">
<item-type>Number:Length</item-type>
<label>Yearly Rain</label>
<description>Rain since the start of this year.</description>
<category>Rain</category>
<tags>
<tag>Measurement</tag>
<tag>Rain</tag>
</tags>
<state readOnly="true" pattern="%.2f %unit%"/>
</channel-type>
<channel-type id="metar" advanced="true">
<item-type>String</item-type>
<label>METAR Weather Report</label>
<description>METAR formatted weather report</description>
<category>Sun_Clouds</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="clouds" advanced="true">
<item-type>String</item-type>
<label>Cloud Cover</label>
<description>METAR style cloud cover.</description>
<category>Sun_Clouds</category>
<state readOnly="true"/>
</channel-type>
<!-- for sensors 2,3,4 use soilmoisture2, soilmoisture3, and soilmoisture4 -->
<channel-type id="soil-moisture" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Soil Moisture</label>
<description>Soil moisture in %.</description>
<category>Moisture</category>
<tags>
<tag>Measurement</tag>
<tag>Water</tag>
</tags>
<state readOnly="true" pattern="%.0f %%"/>
</channel-type>
<!-- for sensor 2 use leafwetness2 -->
<channel-type id="leafwetness" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Leaf Wetness</label>
<description>Leaf wetness in %.</description>
<category>Moisture</category>
<tags>
<tag>Measurement</tag>
<tag>Water</tag>
</tags>
<state readOnly="true" pattern="%.0f %%"/>
</channel-type>
<channel-type id="solarradiation">
<item-type>Number:Intensity</item-type>
<label>Solar Radiation</label>
<description>Solar radiation</description>
<category>Sun</category>
<tags>
<tag>Measurement</tag>
<tag>Light</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="uv">
<item-type>Number:Dimensionless</item-type>
<label>UV Index</label>
<description>UV index.</description>
<category>Sun</category>
<tags>
<tag>Measurement</tag>
<tag>Ultraviolet</tag>
</tags>
<state readOnly="true" pattern="%.0f"/>
</channel-type>
<channel-type id="visibility" advanced="true">
<item-type>Number:Length</item-type>
<label>Visibility</label>
<description>Visibility.</description>
<category>Sun_Clouds</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<!-- Pollution Fields: -->
<channel-type id="nitric-oxide" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Nitric Oxide</label>
<description>Nitric Oxide ppm.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="nitrogen-dioxide-measured" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Nitrogen Dioxide</label>
<description>Nitrogen Dioxide, true measure ppb.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="nitrogen-dioxide-nox-no" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>NO2 X computed</label>
<description>NO2 computed, NOx-NO ppb.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="nitrogen-dioxide-noy-no" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>NO2 Y computed, NOy-NO ppb</label>
<description>NO2 computed, NOy-NO ppb.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="nitrogen-oxides" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Nitrogen Oxides</label>
<description>Nitrogen Oxides ppb.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="total-reactive-nitrogen" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Total Reactive Nitrogen</label>
<description>Total reactive nitrogen.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="no3-ion" advanced="true">
<item-type>Number:Density</item-type>
<label>NO3 ion</label>
<description>NO3 ion (nitrate, not adjusted for ammonium ion) µG/m3.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="so4-ion" advanced="true">
<item-type>Number:Density</item-type>
<label>SO4 ion</label>
<description>SO4 ion (sulfate, not adjusted for ammonium ion) µG/m3.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="sulfur-dioxide" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Sulfur Dioxide</label>
<description>Sulfur Dioxide, conventional ppb.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="sulfur-dioxide-trace-levels" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Sulfur Dioxide Trace Levels</label>
<description>Sulfur Dioxide, trace levels ppb.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="carbon-monoxide" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Carbon Monoxide</label>
<description>Carbon Monoxide, conventional ppm.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="carbon-monoxide-trace-levels" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Carbon Monoxide Trace Levels</label>
<description>Carbon Monoxide, trace levels ppb.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="elemental-carbon" advanced="true">
<item-type>Number:Density</item-type>
<label>Elemental Carbon</label>
<description>Elemental Carbon, PM2.5 µG/m3.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="organic-carbon" advanced="true">
<item-type>Number:Density</item-type>
<label>Organic Carbon</label>
<description>Organic Carbon, not adjusted for oxygen and hydrogen, PM2.5 µG/m3.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="black-carbon" advanced="true">
<item-type>Number:Density</item-type>
<label>Black Carbon</label>
<description>Black Carbon at 880 nm, µG/m3.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="aethalometer" advanced="true">
<item-type>Number:Density</item-type>
<label>Second Channel of Aethalometer</label>
<description>second channel of Aethalometer at 370 nm, µG/m3.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="pm2_5-mass" advanced="true">
<item-type>Number:Density</item-type>
<label>PM2.5 Mass</label>
<description>PM2.5 mass, µG/m3.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="pm10-mass" advanced="true">
<item-type>Number:Density</item-type>
<label>PM10 Mass</label>
<description>PM10 mass, µG/m3.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="ozone" advanced="true">
<item-type>Number:Dimensionless</item-type>
<label>Ozone</label>
<description>Ozone, ppb.</description>
<category>Pollution</category>
<tags>
<tag>Measurement</tag>
</tags>
<state readOnly="true"/>
</channel-type>
<channel-type id="softwaretype" advanced="true">
<item-type>String</item-type>
<label>Software Type</label>
<description>A software type string from the weather station</description>
<category>Text</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="realtime-frequency" advanced="true">
<item-type>Number</item-type>
<label>Realtime Frequency</label>
<description>How often does the weather station submit measurements</description>
<category>Number</category>
<state readOnly="true" pattern="%.0f"/>
</channel-type>
<channel-type id="last-query-state" advanced="true">
<item-type>String</item-type>
<kind>state</kind>
<label>The last query</label>
<description>The part of the last query after the first unurlencoded '?'</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="last-query-trigger">
<item-type>String</item-type>
<kind>trigger</kind>
<label>The last query</label>
<description>The part of the last query after the first unurlencoded '?'</description>
<event/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="wundergroundupdatereceiver"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="wundergroundUpdateReceiver">
<label>Update Receiver</label>
<description>An endpoint thing that can receive and propagate HTTP GET updates meant for a particular station id at
wunderground.com</description>
<channel-groups>
<channel-group id="metadata" typeId="metadata-group"/>
<channel-group id="wind" typeId="wind-group"/>
<channel-group id="temperature" typeId="temperature-group"/>
<channel-group id="humidity" typeId="humidity-group"/>
<channel-group id="rain" typeId="rain-group"/>
<channel-group id="sunlight" typeId="sunlight-group"/>
<channel-group id="pressure" typeId="pressure-group"/>
<channel-group id="pollution" typeId="pollution-group"/>
</channel-groups>
<representation-property>stationId</representation-property>
<config-description>
<parameter name="stationId" type="text" required="true" pattern="\w+">
<label>Station ID</label>
<description><![CDATA[<br />
The wunderground.com update api requires a station id, that is defined for the WeatherUnderground
account measurements are to be submitted to.<br />
<br />
In this binding it is used to identify a unique thing, so each weather-station or
other apparatus submitting measurements can have a separate id, but if you don't intend to forward
the observations to wunderground.com, this value can be any non-blank string.
]]>
</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,256 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.core.Is.is;
import static org.mockito.Mockito.*;
import static org.mockito.MockitoAnnotations.openMocks;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.*;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.DefaultSystemChannelTypeProvider;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.internal.type.StateChannelTypeBuilderImpl;
import org.openhab.core.thing.internal.type.TriggerChannelTypeBuilderImpl;
import org.openhab.core.thing.type.ChannelTypeProvider;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.openhab.core.thing.type.ChannelTypeUID;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
/**
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault({})
class WundergroundUpdateReceiverDiscoveryServiceTest {
private static final String STATION_ID_1 = "abcd1234";
private static final String REQ_STATION_ID = "dfggger";
private static final ThingUID TEST_THING_UID = new ThingUID(THING_TYPE_UPDATE_RECEIVER, "test-receiver");
@BeforeEach
public void setUp() {
openMocks(this);
}
@Test
void aRequestWithAnUnregisteredStationidIsAddedToTheQueueOnce()
throws ServletException, NamespaceException, IOException {
// Given
final String queryString = "ID=dfggger&" + "PASSWORD=XXXXXX&" + "tempf=26.1&" + "humidity=74&" + "dewptf=18.9&"
+ "windchillf=26.1&" + "winddir=14&" + "windspeedmph=1.34&" + "windgustmph=2.46&" + "rainin=0.00&"
+ "dailyrainin=0.00&" + "weeklyrainin=0.00&" + "monthlyrainin=0.08&" + "yearlyrainin=3.06&"
+ "solarradiation=42.24&" + "UV=1&indoortempf=69.3&" + "indoorhumidity=32&" + "baromin=30.39&"
+ "AqNOX=21&" + "lowbatt=1&" + "dateutc=2021-02-07%2014:04:03&" + "softwaretype=WH2600%20V2.2.8&"
+ "action=updateraw&" + "realtime=1&" + "rtfreq=5";
WundergroundUpdateReceiverDiscoveryService discoveryService = mock(
WundergroundUpdateReceiverDiscoveryService.class);
HttpService httpService = mock(HttpService.class);
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
WundergroundUpdateReceiverHandler handler = mock(WundergroundUpdateReceiverHandler.class);
when(handler.getStationId()).thenReturn(STATION_ID_1);
sut.addHandler(handler);
when(discoveryService.isBackgroundDiscoveryEnabled()).thenReturn(false);
// Then
verify(httpService).registerServlet(eq(WundergroundUpdateReceiverServlet.SERVLET_URL), eq(sut), any(), any());
assertThat(sut.isActive(), is(true));
HttpChannel httpChannel = mock(HttpChannel.class);
MetaData.Request request = new MetaData.Request("GET",
new HttpURI("http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + queryString),
HttpVersion.HTTP_1_1, new HttpFields());
Request req = new Request(httpChannel, null);
req.setMetaData(request);
// When
sut.doGet(req, mock(HttpServletResponse.class, Answers.RETURNS_MOCKS));
// Then
verify(handler, never()).updateChannelStates(any());
verify(discoveryService).addUnhandledStationId(eq("dfggger"), any());
assertThat(sut.isActive(), is(true));
}
@Test
void multipleIndexedParametersOfTheSameChanneltypeAreCorrectlyDiscovered() throws IOException {
// Given
final String queryString = "ID=dfggger&" + "PASSWORD=XXXXXX&" + "temp1f=26.1&" + "humidity=74&" + "temp2f=25.1&"
+ "lowbatt=1&" + "soilmoisture1=78&" + "soilmoisture2=73&" + "dateutc=2021-02-07%2014:04:03&"
+ "softwaretype=WH2600%20V2.2.8&" + "action=updateraw&" + "realtime=1&" + "rtfreq=5";
MetaData.Request request = new MetaData.Request("GET",
new HttpURI("http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + queryString),
HttpVersion.HTTP_1_1, new HttpFields());
HttpChannel httpChannel = mock(HttpChannel.class);
Request req = new Request(httpChannel, null);
req.setMetaData(request);
TestChannelTypeRegistry channelTypeRegistry = new TestChannelTypeRegistry();
WundergroundUpdateReceiverDiscoveryService discoveryService = new WundergroundUpdateReceiverDiscoveryService(
false);
discoveryService.addUnhandledStationId(REQ_STATION_ID, req.getParameterMap());
HttpService httpService = mock(HttpService.class);
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
Thing thing = ThingBuilder.create(SUPPORTED_THING_TYPES_UIDS.stream().findFirst().get(), TEST_THING_UID)
.withConfiguration(new Configuration(Map.of(REPRESENTATION_PROPERTY, REQ_STATION_ID)))
.withLabel("test thing").withLocation("location").build();
WundergroundUpdateReceiverHandler handler = new WundergroundUpdateReceiverHandler(thing, sut, discoveryService,
new WundergroundUpdateReceiverUnknownChannelTypeProvider(), channelTypeRegistry);
handler.setCallback(mock(ThingHandlerCallback.class));
handler.initialize();
sut.addHandler(handler);
// When
sut.activate();
// Then
assertThat(sut.isActive(), is(true));
// When
sut.doGet(req, mock(HttpServletResponse.class, Answers.RETURNS_MOCKS));
// Then
assertThat(sut.getHandlers().size(), is(1));
assertThat(sut.getHandlers().containsKey(REQ_STATION_ID), is(true));
assertThat(handler.getThing().getChannels().stream()
.filter(channel -> channel.getChannelTypeUID() == TEMPERATURE_CHANNELTYPEUID).count(), is(2L));
}
@Test
void unregisteredChannelsAreAddedOnTheFlyWhenDiscovered() throws IOException {
// Given
final String firstDeviceQueryString = "ID=dfggger&" + "PASSWORD=XXXXXX&" + "tempf=26.1&" + "humidity=74&"
+ "dateutc=2021-02-07%2014:04:03&" + "softwaretype=WH2600%20V2.2.8&" + "action=updateraw&"
+ "realtime=1&" + "rtfreq=5";
MetaData.Request request1 = new MetaData.Request("GET", new HttpURI(
"http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + firstDeviceQueryString),
HttpVersion.HTTP_1_1, new HttpFields());
HttpChannel httpChannel = mock(HttpChannel.class);
Request req1 = new Request(httpChannel, null);
req1.setMetaData(request1);
TestChannelTypeRegistry channelTypeRegistry = new TestChannelTypeRegistry();
WundergroundUpdateReceiverDiscoveryService discoveryService = new WundergroundUpdateReceiverDiscoveryService(
true);
discoveryService.addUnhandledStationId(REQ_STATION_ID, req1.getParameterMap());
HttpService httpService = mock(HttpService.class);
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
Thing thing = ThingBuilder.create(SUPPORTED_THING_TYPES_UIDS.stream().findFirst().get(), TEST_THING_UID)
.withConfiguration(new Configuration(Map.of(REPRESENTATION_PROPERTY, REQ_STATION_ID)))
.withLabel("test thing").withLocation("location").build();
WundergroundUpdateReceiverHandler handler = new WundergroundUpdateReceiverHandler(thing, sut, discoveryService,
new WundergroundUpdateReceiverUnknownChannelTypeProvider(), channelTypeRegistry);
handler.setCallback(mock(ThingHandlerCallback.class));
// When
handler.initialize();
sut.addHandler(handler);
// Then
ChannelTypeUID[] expectedBefore = new ChannelTypeUID[] { TEMPERATURE_CHANNELTYPEUID, HUMIDITY_CHANNELTYPEUID,
DATEUTC_CHANNELTYPEUID, SOFTWARETYPE_CHANNELTYPEUID, REALTIME_FREQUENCY_CHANNELTYPEUID,
LAST_QUERY_STATE_CHANNELTYPEUID, LAST_RECEIVED_DATETIME_CHANNELTYPEUID,
LAST_QUERY_TRIGGER_CHANNELTYPEUID };
List<ChannelTypeUID> before = handler.getThing().getChannels().stream().map(Channel::getChannelTypeUID)
.collect(Collectors.toList());
assertThat(before, hasItems(expectedBefore));
// When
final String secondDeviceQueryString = "ID=dfggger&" + "PASSWORD=XXXXXX&" + "lowbatt=1&" + "soilmoisture1=78&"
+ "soilmoisture2=73&" + "solarradiation=42.24&" + "dateutc=2021-02-07%2014:04:03&"
+ "softwaretype=WH2600%20V2.2.8&" + "action=updateraw&" + "realtime=1&" + "rtfreq=5";
MetaData.Request request = new MetaData.Request("GET", new HttpURI(
"http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + secondDeviceQueryString),
HttpVersion.HTTP_1_1, new HttpFields());
Request req2 = new Request(httpChannel, null);
req2.setMetaData(request);
sut.activate();
// Then
assertThat(sut.isActive(), is(true));
// When
sut.doGet(req2, mock(HttpServletResponse.class, Answers.RETURNS_MOCKS));
// Then
ChannelTypeUID[] expectedActual = Arrays.copyOf(expectedBefore, expectedBefore.length + 3);
System.arraycopy(new ChannelTypeUID[] { LOW_BATTERY_CHANNELTYPEUID, SOIL_MOISTURE_CHANNELTYPEUID,
SOLARRADIATION_CHANNELTYPEUID }, 0, expectedActual, expectedBefore.length, 3);
List<ChannelTypeUID> actual = handler.getThing().getChannels().stream().map(Channel::getChannelTypeUID)
.collect(Collectors.toList());
assertThat(actual, hasItems(expectedActual));
}
class TestChannelTypeRegistry extends ChannelTypeRegistry {
TestChannelTypeRegistry() {
super();
ChannelTypeProvider provider = mock(ChannelTypeProvider.class);
when(provider.getChannelType(eq(SOFTWARETYPE_CHANNELTYPEUID), any())).thenReturn(
new StateChannelTypeBuilderImpl(SOFTWARETYPE_CHANNELTYPEUID, "Software type", "String").build());
when(provider.getChannelType(eq(TEMPERATURE_CHANNELTYPEUID), any()))
.thenReturn(DefaultSystemChannelTypeProvider.SYSTEM_OUTDOOR_TEMPERATURE);
when(provider.getChannelType(eq(SOIL_MOISTURE_CHANNELTYPEUID), any()))
.thenReturn(new StateChannelTypeBuilderImpl(SOIL_MOISTURE_CHANNELTYPEUID, "Soilmoisture",
"Number:Dimensionless").build());
when(provider.getChannelType(eq(SOLARRADIATION_CHANNELTYPEUID), any()))
.thenReturn(new StateChannelTypeBuilderImpl(SOLARRADIATION_CHANNELTYPEUID, "Solar Radiation",
"Number:Intensity").build());
when(provider.getChannelType(eq(HUMIDITY_CHANNELTYPEUID), any())).thenReturn(
new StateChannelTypeBuilderImpl(HUMIDITY_CHANNELTYPEUID, "Humidity", "Number:Dimensionless")
.build());
when(provider.getChannelType(eq(DATEUTC_CHANNELTYPEUID), any())).thenReturn(
new StateChannelTypeBuilderImpl(DATEUTC_CHANNELTYPEUID, "Last Updated", "String").build());
when(provider.getChannelType(eq(LOW_BATTERY_CHANNELTYPEUID), any()))
.thenReturn(DefaultSystemChannelTypeProvider.SYSTEM_CHANNEL_LOW_BATTERY);
when(provider.getChannelType(eq(REALTIME_FREQUENCY_CHANNELTYPEUID), any())).thenReturn(
new StateChannelTypeBuilderImpl(REALTIME_FREQUENCY_CHANNELTYPEUID, "Realtime frequency", "Number")
.build());
when(provider.getChannelType(eq(LAST_QUERY_STATE_CHANNELTYPEUID), any())).thenReturn(
new StateChannelTypeBuilderImpl(LAST_QUERY_STATE_CHANNELTYPEUID, "The last query", "String")
.build());
when(provider.getChannelType(eq(LAST_RECEIVED_DATETIME_CHANNELTYPEUID), any())).thenReturn(
new StateChannelTypeBuilderImpl(LAST_RECEIVED_DATETIME_CHANNELTYPEUID, "Last Received", "DateTime")
.build());
when(provider.getChannelType(eq(LAST_QUERY_TRIGGER_CHANNELTYPEUID), any())).thenReturn(
new TriggerChannelTypeBuilderImpl(LAST_QUERY_TRIGGER_CHANNELTYPEUID, "The last query").build());
this.addChannelTypeProvider(provider);
this.addChannelTypeProvider(new WundergroundUpdateReceiverUnknownChannelTypeProvider());
}
}
}

View File

@ -0,0 +1,489 @@
/**
* 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.wundergroundupdatereceiver.internal;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsIterableContaining.hasItems;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.MockitoAnnotations.openMocks;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.DATEUTC_DATETIME_CHANNELTYPEUID;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.HUMIDITY_GROUP;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.LAST_QUERY_STATE_CHANNELTYPEUID;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.LAST_QUERY_TRIGGER_CHANNELTYPEUID;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.LAST_RECEIVED_DATETIME_CHANNELTYPEUID;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.METADATA_GROUP;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.POLLUTION_GROUP;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.PRESSURE_GROUP;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.RAIN_GROUP;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.SUNLIGHT_GROUP;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.TEMPERATURE_GROUP;
import static org.openhab.binding.wundergroundupdatereceiver.internal.WundergroundUpdateReceiverBindingConstants.WIND_GROUP;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpURI;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.openhab.core.config.core.Configuration;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.ImperialUnits;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.ThingHandlerCallback;
import org.openhab.core.thing.binding.builder.ChannelBuilder;
import org.openhab.core.thing.binding.builder.ThingBuilder;
import org.openhab.core.thing.type.ChannelKind;
import org.openhab.core.thing.type.ChannelTypeBuilder;
import org.openhab.core.thing.type.ChannelTypeRegistry;
import org.osgi.service.http.HttpService;
import org.osgi.service.http.NamespaceException;
/**
* @author Daniel Demus - Initial contribution
*/
@NonNullByDefault({})
class WundergroundUpdateReceiverServletTest {
private static final String STATION_ID_1 = "abcd1234";
private static final String STATION_ID_2 = "1234abcd";
private static final String REQ_STATION_ID = "dfggger";
private static final ThingUID TEST_THING_UID = new ThingUID(
WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER, "test-receiver");
private @Mock HttpService httpService;
private @Mock ChannelTypeRegistry channelTypeRegistry;
private @Mock WundergroundUpdateReceiverDiscoveryService discoveryService;
@BeforeEach
public void setUp() {
openMocks(this);
}
@Test
void theServletIsActiveAfterTheFirstHandlerIsAdded() throws ServletException, NamespaceException {
// Given
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
WundergroundUpdateReceiverHandler handler = mock(WundergroundUpdateReceiverHandler.class);
when(handler.getStationId()).thenReturn(STATION_ID_1);
// When
sut.addHandler(handler);
// Then
verify(httpService).registerServlet(eq(WundergroundUpdateReceiverServlet.SERVLET_URL), eq(sut), any(), any());
assertThat(sut.isActive(), is(true));
}
@Test
void theServletIsInactiveAfterTheLastHandlerIsRemovedAndBackgroundDiscoveryIsDisabled()
throws ServletException, NamespaceException {
// Given
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
WundergroundUpdateReceiverHandler handler = mock(WundergroundUpdateReceiverHandler.class);
when(handler.getStationId()).thenReturn(STATION_ID_1);
when(discoveryService.isBackgroundDiscoveryEnabled()).thenReturn(false);
// When
sut.addHandler(handler);
// Then
verify(httpService).registerServlet(eq(WundergroundUpdateReceiverServlet.SERVLET_URL), eq(sut), any(), any());
assertThat(sut.isActive(), is(true));
// When
sut.removeHandler(handler.getStationId());
// Then
verify(httpService).unregister(WundergroundUpdateReceiverServlet.SERVLET_URL);
assertThat(sut.isActive(), is(false));
}
@Test
void theServletIsActiveAfterTheLastHandlerIsRemovedButBackgroundDiscoveryIsEnabled()
throws ServletException, NamespaceException {
// Given
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
WundergroundUpdateReceiverHandler handler = mock(WundergroundUpdateReceiverHandler.class);
when(handler.getStationId()).thenReturn(STATION_ID_1);
when(discoveryService.isBackgroundDiscoveryEnabled()).thenReturn(true);
// When
sut.addHandler(handler);
// Then
verify(httpService).registerServlet(eq(WundergroundUpdateReceiverServlet.SERVLET_URL), eq(sut), any(), any());
assertThat(sut.isActive(), is(true));
// When
sut.removeHandler(handler.getStationId());
// Then
verify(httpService, never()).unregister(WundergroundUpdateReceiverServlet.SERVLET_URL);
assertThat(sut.isActive(), is(true));
}
@Test
void onDisposeAllHandlersAreRemovedAndServletIsInactive() throws ServletException, NamespaceException {
// Given
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
WundergroundUpdateReceiverHandler handler1 = mock(WundergroundUpdateReceiverHandler.class);
when(handler1.getStationId()).thenReturn(STATION_ID_1);
WundergroundUpdateReceiverHandler handler2 = mock(WundergroundUpdateReceiverHandler.class);
when(handler2.getStationId()).thenReturn(STATION_ID_2);
// When
sut.addHandler(handler1);
sut.addHandler(handler2);
// Then
verify(httpService).registerServlet(eq(WundergroundUpdateReceiverServlet.SERVLET_URL), eq(sut), any(), any());
assertThat(sut.isActive(), is(true));
// When
sut.dispose();
// Then
verify(httpService, times(2)).unregister(WundergroundUpdateReceiverServlet.SERVLET_URL);
assertThat(sut.isActive(), is(false));
}
@Test
void OnDisposeAllHandlersAreRemovedAndServletIsInactiveEvenThoughBackgroundDiscoveryIsEnabled()
throws ServletException, NamespaceException {
// Given
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
WundergroundUpdateReceiverHandler handler1 = mock(WundergroundUpdateReceiverHandler.class);
when(handler1.getStationId()).thenReturn(STATION_ID_1);
WundergroundUpdateReceiverHandler handler2 = mock(WundergroundUpdateReceiverHandler.class);
when(handler2.getStationId()).thenReturn(STATION_ID_2);
when(discoveryService.isBackgroundDiscoveryEnabled()).thenReturn(true);
// When
sut.addHandler(handler1);
sut.addHandler(handler2);
// Then
verify(httpService).registerServlet(eq(WundergroundUpdateReceiverServlet.SERVLET_URL), eq(sut), any(), any());
assertThat(sut.isActive(), is(true));
// When
sut.dispose();
// Then
verify(httpService).unregister(WundergroundUpdateReceiverServlet.SERVLET_URL);
assertThat(sut.isActive(), is(false));
}
@Test
void changedStationIdPropagatesToHandlerKey() throws ServletException, NamespaceException {
// Given
Thing thing = mock(Thing.class);
when(thing.getUID()).thenReturn(TEST_THING_UID);
when(thing.getConfiguration()).thenReturn(new Configuration(
Map.of(WundergroundUpdateReceiverBindingConstants.REPRESENTATION_PROPERTY, STATION_ID_1)));
when(thing.getStatus()).thenReturn(ThingStatus.ONLINE);
when(this.channelTypeRegistry.getChannelType(LAST_RECEIVED_DATETIME_CHANNELTYPEUID))
.thenReturn(ChannelTypeBuilder.state(LAST_RECEIVED_DATETIME_CHANNELTYPEUID, "Label", "String").build());
when(this.channelTypeRegistry.getChannelType(DATEUTC_DATETIME_CHANNELTYPEUID))
.thenReturn(ChannelTypeBuilder.state(DATEUTC_DATETIME_CHANNELTYPEUID, "Label", "DateTime").build());
when(this.channelTypeRegistry.getChannelType(LAST_QUERY_STATE_CHANNELTYPEUID))
.thenReturn(ChannelTypeBuilder.state(LAST_QUERY_STATE_CHANNELTYPEUID, "Label", "String").build());
when(this.channelTypeRegistry.getChannelType(LAST_QUERY_TRIGGER_CHANNELTYPEUID))
.thenReturn(ChannelTypeBuilder.trigger(LAST_QUERY_TRIGGER_CHANNELTYPEUID, "Label").build());
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
WundergroundUpdateReceiverHandler handler = new WundergroundUpdateReceiverHandler(thing, sut, discoveryService,
new WundergroundUpdateReceiverUnknownChannelTypeProvider(), channelTypeRegistry);
ThingHandlerCallback mockCallback = mock(ThingHandlerCallback.class);
handler.setCallback(mockCallback);
// When
handler.initialize();
// Then
verify(httpService).registerServlet(eq(WundergroundUpdateReceiverServlet.SERVLET_URL), eq(sut), any(), any());
assertThat(sut.isActive(), is(true));
assertThat(sut.getStationIds(), hasItems(STATION_ID_1));
// When
handler.handleConfigurationUpdate(
Map.of(WundergroundUpdateReceiverBindingConstants.REPRESENTATION_PROPERTY, STATION_ID_2));
// Then
assertThat(sut.isActive(), is(true));
ArgumentCaptor<Thing> thingArg = ArgumentCaptor.forClass(Thing.class);
verify(mockCallback).configurationUpdated(thingArg.capture());
assertThat(thingArg.getValue().getConfiguration().getProperties()
.get(WundergroundUpdateReceiverBindingConstants.REPRESENTATION_PROPERTY), is(STATION_ID_2));
}
@Test
void aGetRequestIsCorrectlyParsed() throws IOException {
// Given
ThingUID testThingUID = new ThingUID(WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER,
"test-receiver");
final String queryString = "ID=dfggger&PASSWORD=XXXXXX&tempf=26.1&humidity=74&dewptf=18.9&windchillf=26.1&winddir=14&windspeedmph=1.34&windgustmph=2.46&rainin=0.00&dailyrainin=0.00&weeklyrainin=0.00&monthlyrainin=0.08&yearlyrainin=3.06&solarradiation=42.24&UV=1&indoortempf=69.3&indoorhumidity=32&baromin=30.39&AqNOX=21&lowbatt=1&dateutc=2021-02-07%2014:04:03&softwaretype=WH2600%20V2.2.8&action=updateraw&realtime=1&rtfreq=5";
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
List<Channel> channels = List.of(
ChannelBuilder
.create(new ChannelUID(testThingUID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.DATEUTC), "String")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.REALTIME_FREQUENCY), "Number")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LOW_BATTERY), "Switch")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, WIND_GROUP,
WundergroundUpdateReceiverBindingConstants.WIND_DIRECTION), "Number:Angle")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, WIND_GROUP,
WundergroundUpdateReceiverBindingConstants.WIND_SPEED), "Number:Speed")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, WIND_GROUP,
WundergroundUpdateReceiverBindingConstants.GUST_SPEED), "Number:Speed")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, TEMPERATURE_GROUP,
WundergroundUpdateReceiverBindingConstants.TEMPERATURE), "Number:Temperature")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, RAIN_GROUP,
WundergroundUpdateReceiverBindingConstants.RAIN_IN), "Number:Length")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, SUNLIGHT_GROUP,
WundergroundUpdateReceiverBindingConstants.SOLAR_RADIATION), "Number:Intensity")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, SUNLIGHT_GROUP,
WundergroundUpdateReceiverBindingConstants.UV), "Number")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, PRESSURE_GROUP,
WundergroundUpdateReceiverBindingConstants.BAROM_IN), "Number:Pressure")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, HUMIDITY_GROUP,
WundergroundUpdateReceiverBindingConstants.DEWPOINT), "Number:Temperature")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, HUMIDITY_GROUP,
WundergroundUpdateReceiverBindingConstants.HUMIDITY), "Number:Dimensionless")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, POLLUTION_GROUP,
WundergroundUpdateReceiverBindingConstants.AQ_NOX), "Number:Dimensionless")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LAST_QUERY_TRIGGER), "StringType")
.withKind(ChannelKind.TRIGGER).build());
Configuration config = new Configuration(Map.of("stationId", REQ_STATION_ID));
Thing testThing = ThingBuilder
.create(WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER, testThingUID)
.withChannels(channels).withConfiguration(config).build();
ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
WundergroundUpdateReceiverHandler handler = new WundergroundUpdateReceiverHandler(testThing, sut,
discoveryService, new WundergroundUpdateReceiverUnknownChannelTypeProvider(), channelTypeRegistry);
handler.setCallback(callback);
handler.initialize();
HttpChannel httpChannel = mock(HttpChannel.class);
MetaData.Request request = new MetaData.Request("GET",
new HttpURI("http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + queryString),
HttpVersion.HTTP_1_1, new HttpFields());
Request req = new Request(httpChannel, null);
req.setMetaData(request);
// When
sut.doGet(req, mock(HttpServletResponse.class, Answers.RETURNS_MOCKS));
// Then
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, METADATA_GROUP, WundergroundUpdateReceiverBindingConstants.DATEUTC),
StringType.valueOf("2021-02-07 14:04:03"));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, METADATA_GROUP, WundergroundUpdateReceiverBindingConstants.LOW_BATTERY),
OnOffType.ON);
verify(callback).stateUpdated(new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.REALTIME_FREQUENCY), new DecimalType(5));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, WIND_GROUP, WundergroundUpdateReceiverBindingConstants.WIND_DIRECTION),
new QuantityType<>(14, Units.DEGREE_ANGLE));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, WIND_GROUP, WundergroundUpdateReceiverBindingConstants.WIND_SPEED),
new QuantityType<>(1.34, ImperialUnits.MILES_PER_HOUR));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, WIND_GROUP, WundergroundUpdateReceiverBindingConstants.GUST_SPEED),
new QuantityType<>(2.46, ImperialUnits.MILES_PER_HOUR));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, TEMPERATURE_GROUP,
WundergroundUpdateReceiverBindingConstants.TEMPERATURE),
new QuantityType<>(26.1, ImperialUnits.FAHRENHEIT));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, RAIN_GROUP, WundergroundUpdateReceiverBindingConstants.RAIN_IN),
new QuantityType<>(0, ImperialUnits.INCH));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, SUNLIGHT_GROUP,
WundergroundUpdateReceiverBindingConstants.SOLAR_RADIATION),
new QuantityType<>(42.24, Units.IRRADIANCE));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, SUNLIGHT_GROUP, WundergroundUpdateReceiverBindingConstants.UV),
new DecimalType(1));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, PRESSURE_GROUP, WundergroundUpdateReceiverBindingConstants.BAROM_IN),
new QuantityType<>(30.39, ImperialUnits.INCH_OF_MERCURY));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, HUMIDITY_GROUP, WundergroundUpdateReceiverBindingConstants.DEWPOINT),
new QuantityType<>(18.9, ImperialUnits.FAHRENHEIT));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, HUMIDITY_GROUP, WundergroundUpdateReceiverBindingConstants.HUMIDITY),
new QuantityType<>(74, Units.PERCENT));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, POLLUTION_GROUP, WundergroundUpdateReceiverBindingConstants.AQ_NOX),
new QuantityType<>(21, Units.PARTS_PER_BILLION));
verify(callback, never()).stateUpdated(new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.SOFTWARE_TYPE), StringType.valueOf("WH2600 V2.2.8"));
verify(callback).stateUpdated(new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LAST_QUERY_STATE), StringType.valueOf(queryString));
verify(callback).stateUpdated(eq(new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LAST_RECEIVED)), any(DateTimeType.class));
verify(callback).channelTriggered(testThing, new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LAST_QUERY_TRIGGER), queryString);
}
@Test
void aGetRequestWithIndexedParametresAreCorrectlyParsed() throws IOException {
// Given
ThingUID testThingUID = new ThingUID(WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER,
"test-receiver");
final String queryString = "ID=dfggger&PASSWORD=XXXXXX&temp1f=26.1&humidity=74&temp2f=25.1&lowbatt=1&soilmoisture1=78&soilmoisture2=73&dateutc=2021-02-07%2014:04:03&softwaretype=WH2600%20V2.2.8&action=updateraw&realtime=1&rtfreq=5";
WundergroundUpdateReceiverServlet sut = new WundergroundUpdateReceiverServlet(httpService, discoveryService);
List<Channel> channels = List.of(
ChannelBuilder
.create(new ChannelUID(testThingUID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.DATEUTC), "String")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.REALTIME_FREQUENCY), "Number")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LOW_BATTERY), "Switch")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder.create(new ChannelUID(testThingUID, TEMPERATURE_GROUP, "temp1f"), "Number:Temperature")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder.create(new ChannelUID(testThingUID, TEMPERATURE_GROUP, "temp2f"), "Number:Temperature")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, HUMIDITY_GROUP, "soilmoisture1"), "Number:Dimensionless")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, HUMIDITY_GROUP, "soilmoisture2"), "Number:Dimensionless")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, HUMIDITY_GROUP,
WundergroundUpdateReceiverBindingConstants.HUMIDITY), "Number:Dimensionless")
.withKind(ChannelKind.STATE).build(),
ChannelBuilder
.create(new ChannelUID(testThingUID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LAST_QUERY_TRIGGER), "StringType")
.withKind(ChannelKind.TRIGGER).build());
Configuration config = new Configuration(Map.of("stationId", REQ_STATION_ID));
Thing testThing = ThingBuilder
.create(WundergroundUpdateReceiverBindingConstants.THING_TYPE_UPDATE_RECEIVER, testThingUID)
.withChannels(channels).withConfiguration(config).build();
ThingHandlerCallback callback = mock(ThingHandlerCallback.class);
WundergroundUpdateReceiverHandler handler = new WundergroundUpdateReceiverHandler(testThing, sut,
discoveryService, new WundergroundUpdateReceiverUnknownChannelTypeProvider(), channelTypeRegistry);
handler.setCallback(callback);
handler.initialize();
HttpChannel httpChannel = mock(HttpChannel.class);
MetaData.Request request = new MetaData.Request("GET",
new HttpURI("http://localhost" + WundergroundUpdateReceiverServlet.SERVLET_URL + "?" + queryString),
HttpVersion.HTTP_1_1, new HttpFields());
Request req = new Request(httpChannel, null);
req.setMetaData(request);
// When
sut.doGet(req, mock(HttpServletResponse.class, Answers.RETURNS_MOCKS));
// Then
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, METADATA_GROUP, WundergroundUpdateReceiverBindingConstants.DATEUTC),
StringType.valueOf("2021-02-07 14:04:03"));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, METADATA_GROUP, WundergroundUpdateReceiverBindingConstants.LOW_BATTERY),
OnOffType.ON);
verify(callback).stateUpdated(new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.REALTIME_FREQUENCY), new DecimalType(5));
verify(callback).stateUpdated(new ChannelUID(TEST_THING_UID, TEMPERATURE_GROUP, "temp1f"),
new QuantityType<>(26.1, ImperialUnits.FAHRENHEIT));
verify(callback).stateUpdated(new ChannelUID(TEST_THING_UID, TEMPERATURE_GROUP, "temp2f"),
new QuantityType<>(25.1, ImperialUnits.FAHRENHEIT));
verify(callback).stateUpdated(
new ChannelUID(TEST_THING_UID, HUMIDITY_GROUP, WundergroundUpdateReceiverBindingConstants.HUMIDITY),
new QuantityType<>(74, Units.PERCENT));
verify(callback).stateUpdated(new ChannelUID(TEST_THING_UID, HUMIDITY_GROUP, "soilmoisture1"),
new QuantityType<>(78, Units.PERCENT));
verify(callback).stateUpdated(new ChannelUID(TEST_THING_UID, HUMIDITY_GROUP, "soilmoisture2"),
new QuantityType<>(73, Units.PERCENT));
verify(callback, never()).stateUpdated(new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.SOFTWARE_TYPE), StringType.valueOf("WH2600 V2.2.8"));
verify(callback).stateUpdated(new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LAST_QUERY_STATE), StringType.valueOf(queryString));
verify(callback).stateUpdated(eq(new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LAST_RECEIVED)), any(DateTimeType.class));
verify(callback).channelTriggered(testThing, new ChannelUID(TEST_THING_UID, METADATA_GROUP,
WundergroundUpdateReceiverBindingConstants.LAST_QUERY_TRIGGER), queryString);
}
}

View File

@ -387,6 +387,7 @@
<module>org.openhab.binding.wlanthermo</module>
<module>org.openhab.binding.wled</module>
<module>org.openhab.binding.wolfsmartset</module>
<module>org.openhab.binding.wundergroundupdatereceiver</module>
<module>org.openhab.binding.xmltv</module>
<module>org.openhab.binding.xmppclient</module>
<module>org.openhab.binding.yamahamusiccast</module>