diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 992b960a5e998..d19f228c8b905 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -1086,6 +1086,11 @@ org.openhab.binding.meteostick ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.metofficedatahub + ${project.version} + org.openhab.addons.bundles org.openhab.binding.mffan diff --git a/bundles/org.openhab.binding.metofficedatahub/NOTICE b/bundles/org.openhab.binding.metofficedatahub/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/NOTICE @@ -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 diff --git a/bundles/org.openhab.binding.metofficedatahub/README.md b/bundles/org.openhab.binding.metofficedatahub/README.md new file mode 100644 index 0000000000000..ca948b58d709e --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/README.md @@ -0,0 +1,446 @@ +# Met Office DataHub Binding + +This binding is for the UK Based Met Office Data Hub, weather service. +Its purpose is to allow the retrieval of forecast (hourly and daily) for a given location (Site). + +The website can be found here: + +**IMPORTANT:** The Met Office Data Hub service is free of charge for low volume users. +Higher data usages are charged, please see their website for current information. +Please bear this in mind before adjust polling rates, or adding more than 1 location (site) for forecast data, as you may need a different plan depending on the data throughput over a month, or API hit rate. + +A possible use case could be to pull forecast data, for the next day to determine if storage heaters or underfloor heating should be pre-heated overnight. + +## Prerequisite + +In order to use this binding, you will need a Met Office Data Hub account. +Once created you will need to create a plan for access to the "Site Specific" subscriptions. +This will give you the client id and secret required for the bridge. + +## Supported Things + +This binding consists of a bridge for connecting to the Met Office Data Hub service with your account. +You can then add things to get the forecast's for a specific location (site), using this bridge. + +This binding supports the follow thing types: + +| Type UID | Discovery | Description | +|-----------|-----------|---------------------------------------------------------------------------------------------| +| bridge | Manual | A single connection to the Met Office DataHub API with daily poll limiting for the Site API | +| site | Manual | Provides the hourly and daily forecast data for a give location (site) | + +## Configuration + +### `bridge` Configuration + +The bridge counts the total number of requests from 00:00 -> 23:59 under its properties during the runtime of the system. +(This reset's if OH restarts, or the binding resets). + +| Name | Type | Description | Default Values | +|--------------------|--------|-------------------------------------------------------------------------------------------|----------------| +| siteRateDailyLimit | Number | This is a daily poll limit for the SiteSpecific API, while the Thing ID remains the same. | 250 | +| siteApiKey | String | The API Key for the Site Specific subscription in your MET Office Data Hub account. | | + +**NOTE:** siteRateDailyLimit: This **should** prevent any more poll's for the rest of the day to the SiteSpecific API, once this limit is reached as a failsafe against a bad configuration, if you don't reboot / delete and re-add the bridge. This is reset at 00:00UTC in-line with MET Office DataHub behaviours. + +### `site` Configuration Parameters + +| Name | Type | Description | Default Values | +|--------------------------|--------|----------------------------------------------------------------|-------------------------------------------------------| +| hourlyForecastPollRate | Number | The number of hours between polling for each sites hourly data | 1 | +| dailyForecastPollRate | Number | The number of hours between polling for each sites daily data | 3 | +| location | String | The lat/long of the site e.g. "51.5072,0.1276" | openHAB's user configured location is used when unset | + +## Channels + +### Hourly Forecast Channels + +| Channel Id | Type | Description | Unit | +|------------------|----------------------|----------------------------------------------|------| +| forecast-ts | String | Time of forecast window start | | +| air-temp-current | Number:Temperature | Air Temperature | °C | +| air-temp-min | Number:Temperature | Minimum Air Temperature Over Previous Hour | °C | +| air-temp-max | Number:Temperature | Maximum Air Temperature Over Previous Hour | °C | +| feels-like | Number:Temperature | Feels Like Temperature | °C | +| humidity | Number:Dimensionless | Relative Humidity | % | +| visibility | Number:Length | Visibility | m | +| precip-rate | Number:Speed | Precipitation Rate | mm/h | +| precip-prob | Number:Dimensionless | Probability of Precipitation | % | +| precip-total | Number:Length | Total Precipitation of Previous Hour | mm | +| snow-total | Number:Length | Total Snowfall of Previous Hour | mm | +| uv-index | Number:Dimensionless | UV Index | | +| pressure | Number:Pressure | Mean Sea Level Pressure | Pa | +| wind-speed | Number:Speed | 10m Wind Speed | m/s | +| wind-gust | Number:Speed | 10m Wind Gust Speed | m/s | +| wind-gust-max | Number:Speed | Maximum 10m Wind Gust Speed of Previous Hour | m/s | +| wind-direction | Number:Angle | 10m Wind From Direction | ° | +| dewpoint | Number:Temperature | Dew Point Temperature | °C | + +This binding uses channel groups. +The channels under "Forecast for the current hour" will be mirrored for future hours forecasts. + +The channel naming follows the following format: + +```current-forecast#air-temp-current``` + +The current hours forecast to get the air-temp-current would be: + +current-forecast#air-temp-current + +1 hour into the future to get the air-temp-current it would be: + +current-forecast-**plus01**#air-temp-current + +2 hour's into the future to get the air-temp-current it would be: + +current-forecast-**plus02**#air-temp-current + +#### Channel Groups for Hourly Forecast Channels + +| Channel Id | Description | +|-------------------------|-------------------------------------------| +| current-forecast | Current hours forecast | +| current-forecast-plus01 | 01 hour after the current hours forecast | +| current-forecast-plus02 | 02 hours after the current hours forecast | +| ....................... | ......................................... | +| current-forecast-plus23 | 23 hours after the current hours forecast | +| current-forecast-plus24 | 24 hours after the current hours forecast | + +### Daily Forecast Channels + +| Channel Id | Type | Unit | MET Office Data Description | +|-------------------------|----------------------|------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| forecast-ts | String | | Calculated from the MET provided UTZ time of when the forecast is applicable, mapped to the local system TZ. | +| wind-speed-day | Number:Speed | m/s | Mean wind speed is equivalent to the mean speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. | +| wind-speed-night | Number:Speed | m/s | Mean wind speed is equivalent to the mean speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. | +| wind-direction-day | Number:Angle | ° | Mean wind direction is equivalent to the mean direction observed over the 10 minutes preceding the validity time. In meteorological reports the direction of the wind vector is given as the direction from which it is blowing. 10m wind is the considered surface wind. | +| wind-direction-night | Number:Angle | ° | Mean wind direction is equivalent to the mean direction observed over the 10 minutes preceding the validity time. In meteorological reports the direction of the wind vector is given as the direction from which it is blowing. 10m wind is the considered surface wind. | +| wind-gust-day | Number:Speed | m/s | The gust speed is equivalent to the maximum 3 second mean wind speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. | +| wind-gust-night | Number:Speed | m/s | The gust speed is equivalent to the maximum 3 second mean wind speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. | +| visibility-day | Number:Length | m | Minimal horizontal distance at which a known object can be seen. | | +| visibility-night | Number:Length | m | Minimal horizontal distance at which a known object can be seen. | | +| humidity-day | Number:Dimensionless | % | Stevenson screen height is approximately 1.5m above ground level. | | +| humidity-night | Number:Dimensionless | % | Stevenson screen height is approximately 1.5m above ground level. | | +| pressure-day | Number:Pressure | Pa | Air pressure at mean sea level which is close to the geoid in sea areas. Air pressure at sea level is the quantity often abbreviated as pressure or PMSL. | +| pressure-night | Number:Pressure | Pa | Air pressure at mean sea level which is close to the geoid in sea areas. Air pressure at sea level is the quantity often abbreviated as pressure or PMSL. | +| uv-max | Number:Dimensionless | | Usually a value from 0 to 13 but higher values are possible in extreme situations. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| temp-max-day | Number:Temperature | °C | This is the most likely maximum value over the day based on the ensemble spread. Stevenson screen height is approximately 1.5m above ground level. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| temp-min-night | Number:Temperature | °C | This is the most likely minimum value over the night based on the ensemble spread. Stevenson screen height is approximately 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. | +| temp-max-lb-day | Number:Temperature | °C | This is the lower bound for the maximum value over the day based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. Stevenson screen height is approximately 1.5m above ground level. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| temp-min-lb-night | Number:Temperature | °C | This is the lower bound for the minimum value over the night based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. Stevenson screen height is approximately 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. | +| temp-max-ub-day | Number:Temperature | °C | This is the upper bound for the maximum value over the day based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% probability that the actual figure will be below this upper bound figure. Stevenson screen height is approximately 1.5m above ground level. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| temp-min-ub-night | Number:Temperature | °C | This is the upper bound for the minimum value over the night based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% probability that the actual figure will be below this upper bound figure. Stevenson screen height is approximately 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. | +| feels-like-max-day | Number:Temperature | °C | This is the most likely maximum value over the day based on the ensemble spread. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| feels-like-min-night | Number:Temperature | °C | This is the most likely minimum value over the night based on the ensemble spread. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Night-time is defined as those forecast times that fall between local dusk and dawn. | +| feels-like-max-lb-day | Number:Temperature | °C | This is the lower bound for the maximum value over the day based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| feels-like-min-lb-night | Number:Temperature | °C | This is the lower bound for the minimum value over the night based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Night-time is defined as those forecast times that fall between local dusk and dawn. | +| feels-like-max-ub-day | Number:Temperature | °C | This is the upper bound for the maximum value over the day based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% probability that the actual figure will be below this upper bound figure. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| feels-like-min-ub-night | Number:Temperature | °C | This is the upper bound for the minimum value over the night based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% probability that the actual figure will be below this upper bound figure. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Night-time is defined as those forecast times that fall between local dusk and dawn. | +| precip-prob-day | Number:Dimensionless | % | Daytime is defined as those forecast times that fall between local dawn and dusk. | +| precip-prob-night | Number:Dimensionless | % | Night-time is defined as those forecast times that fall between local dusk and dawn. | +| snow-prob-day | Number:Dimensionless | % | Daytime is defined as those forecast times that fall between local dawn and dusk. | +| snow-prob-night | Number:Dimensionless | % | Night-time is defined as those forecast times that fall between local dusk and dawn. | +| heavy-snow-prob-day | Number:Dimensionless | % | Heavy snow is defined as >1mm/hr liquid water equivalent and is approximately equivilent to >1cm snow per hour. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| heavy-snow-prob-night | Number:Dimensionless | % | Heavy snow is defined as >1mm/hr liquid water equivalent and is approximately equivilent to >1cm snow per hour. Night-time is defined as those forecast times that fall between local dusk and dawn. | +| rain-prob-day | Number:Dimensionless | % | Daytime is defined as those forecast times that fall between local dawn and dusk. | +| rain-prob-night | Number:Dimensionless | % | Night-time is defined as those forecast times that fall between local dusk and dawn. | +| day-prob-heavy-rain | Number:Dimensionless | % | Heavy rain is defined as >1mm/hr. Daytime is defined as those forecast times that fall between local dawn and dusk. | +| night-prob-heavy-rain | Number:Dimensionless | % | Heavy rain is defined as >1mm/hr. Night-time is defined as those forecast times that fall between local dusk and dawn. | +| hail-prob-day | Number:Dimensionless | % | Daytime is defined as those forecast times that fall between local dawn and dusk. | +| hail-prob-night | Number:Dimensionless | % | Night-time is defined as those forecast times that fall between local dusk and dawn. | +| sferics-prob-day | Number:Dimensionless | % | This is the probability of a strike within a radius of 50km. | +| sferics-prob-night | Number:Dimensionless | % | This is the probability of a strike within a radius of 50km. | + +#### Channel Groups for Daily Forecast Channels + +| Channel Id | Description | +|-----------------------|---------------------------------------------------| +| daily-forecast | This is the weather forecast for the current day. | +| daily-forecast-plus01 | This is the weather forecast in 1 day. | +| daily-forecast-plus02 | This is the weather forecast in 2 days. | +| ..................... | ................................................. | +| daily-forecast-plus05 | This is the weather forecast in 5 days. | +| daily-forecast-plus06 | This is the weather forecast in 6 days. | + +## Full Example + +### Configuration (*.things) + +```java +Bridge metofficedatahub:site:metoffice [siteRateDailyLimit=200, siteApiKey=""] { + site londonForecast "London Forecast" [hourlyForecastPollRate=1, dailyForecastPollRate=3, location="51.509865,-0.118092"] +} +``` + +### Configuration (*.items) + +#### Hourly Forecast `example.items` + +```java +Group gCurrentHourForecast "Current Hour Forecast" +Group gLondon "London" +Group gLondonCurrentHour "London Current Forecast" (gLondon,gCurrentHourForecast) +DateTime ForecastLondonHourlyForecastTs (gLondonCurrentHour) { channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#forecast-ts" } +Number:Temperature ForecastLondonCurrentHour (gLondonCurrentHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#air-temp-current" } +Number:Temperature ForecastLondonMinTemp (gLondonCurrentHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#air-temp-min" } +Number:Temperature ForecastLondonMaxTemp (gLondonCurrentHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#air-temp-max" } +Number:Temperature ForecastLondonFeelsLikeTemp (gLondonCurrentHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#feels-like" } +Number:Dimensionless ForecastLondonRelHumidity (gLondonCurrentHour) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#humidity" } +Number:Length ForecastLondonVisibility (gLondonCurrentHour) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#visibility" } +Number:Dimensionless ForecastLondonPrecipitationProb (gLondonCurrentHour) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#precip-prob" } +Number:Speed ForecastLondonPrecipitationRate (gLondonCurrentHour) { unit="mm/h",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#precip-rate" } +Number:Length ForecastLondonPrecipitationAmount (gLondonCurrentHour) { unit="mm",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#precip-total" } +Number:Length ForecastLondonSnowAmount (gLondonCurrentHour) { unit="mm",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#snow-total" } +Number:Dimensionless ForecastLondonUvIndex (gLondonCurrentHour) { channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#uv-index" } +Number:Pressure ForecastLondonpressure (gLondonCurrentHour) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#pressure" } +Number:Speed ForecastLondon10mWindSpeed (gLondonCurrentHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#wind-speed" } +Number:Speed ForecastLondon10mGustWindSpeed (gLondonCurrentHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#wind-speed-gust" } +Number:Speed ForecastLondon10mMaxGustWindSpeed (gLondonCurrentHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#wind-gust-max" } +Number:Angle ForecastLondon10mWindDirection (gLondonCurrentHour) { unit="°",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#wind-direction" } +Number:Temperature ForecastLondonDewPointTemp (gLondonCurrentHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast#dewpoint" } + +Group gCurrentHourPlus01Forecast "Next Hours Forecast" +Group gLondonNextHour "London Next Hours Forecast" (gLondon,gCurrentHourPlus01Forecast) +DateTime ForecastLondonPlus01HourlyForecastTs (gLondonNextHour) { channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#forecast-ts" } +Number:Temperature ForecastLondonPlus01CurrentHour (gLondonNextHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#air-temp-current" } +Number:Temperature ForecastLondonPlus01MinTemp (gLondonNextHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#air-temp-min" } +Number:Temperature ForecastLondonPlus01MaxTemp (gLondonNextHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#air-temp-max" } +Number:Temperature ForecastLondonPlus01FeelsLikeTemp (gLondonNextHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#feels-like" } +Number:Dimensionless ForecastLondonPlus01RelHumidity (gLondonNextHour) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#humidity" } +Number:Length ForecastLondonPlus01Visibility (gLondonNextHour) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#visibility" } +Number:Speed ForecastLondonPlus01PrecipitationRate (gLondonNextHour) { unit="mm/h",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#precip-rate" } +Number:Dimensionless ForecastLondonPlus01PrecipitationProb (gLondonNextHour) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#precip-prob" } +Number:Length ForecastLondonPlus01PrecipitationAmount (gLondonNextHour) { unit="mm",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#precip-total" } +Number:Length ForecastLondonPlus01SnowAmount (gLondonNextHour) { unit="mm",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#snow-total" } +Number:Dimensionless ForecastLondonPlus01UvIndex (gLondonNextHour) { channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#uv-index" } +Number:Pressure ForecastLondonPlus01pressure (gLondonNextHour) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#pressure" } +Number:Speed ForecastLondonPlus0110mWindSpeed (gLondonNextHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#wind-speed" } +Number:Speed ForecastLondonPlus0110mGustWindSpeed (gLondonNextHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#wind-speed-gust" } +Number:Speed ForecastLondonPlus0110mMaxGustWindSpeed (gLondonNextHour) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#wind-gust-max" } +Number:Angle ForecastLondonPlus0110mWindDirection (gLondonNextHour) { unit="°",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#wind-direction" } +Number:Temperature ForecastLondonPlus01DewPointTemp (gLondonNextHour) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:current-forecast-plus01#dewpoint" } +``` + +#### Daily Forecast `example.items` + +```java +Group gdaily-forecast "Current Daily Forecast" +Group gLondonCurrentDay "London Current Forecast" (gLondon,gdaily-forecast) +DateTime ForecastLondonDailyForecastTs (gLondonCurrentDay) { channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#forecast-ts" } +Number:Speed ForecastLondonMiddayWindSpeed10m (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-speed-day" } +Number:Speed ForecastLondonMidnightWindSpeed10m (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-speed-night" } +Number:Angle ForecastLondonMidday10MWindDirection (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-direction-day" } +Number:Angle ForecastLondonMidnight10MWindDirection (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-direction-night" } +Number:Speed ForecastLondonMidday10mWindGust (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-gust-day" } +Number:Speed ForecastLondonMidnight10mWindGust (gLondonCurrentDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#wind-gust-night" } +Number:Length ForecastLondonMiddayVisibility (gLondonCurrentDay) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#visibility-day" } +Number:Length ForecastLondonMidnightVisibility (gLondonCurrentDay) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#visibility-night" } +Number:Dimensionless ForecastLondonMiddayRelativeHumidity (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#humidity-day" } +Number:Dimensionless ForecastLondonMidnightRelativeHumidity (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#humidity-night" } +Number:Pressure ForecastLondonMiddaypressure (gLondonCurrentDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#pressure-day" } +Number:Pressure ForecastLondonMidnightpressure (gLondonCurrentDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#pressure-night" } +Number:Dimensionless ForecastLondonMaxUvIndex (gLondonCurrentDay) { channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#uv-max" } +Number:Temperature ForecastLondonNightUpperBoundMinTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#temp-min-ub-night" } +Number:Temperature ForecastLondonDayLowerBoundMaxTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#temp-max-lb-day" } +Number:Temperature ForecastLondonNightLowerBoundMinTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#temp-min-lb-night" } +Number:Temperature ForecastLondonDayMaxFeelsLikeTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#feels-like-max-day" } +Number:Temperature ForecastLondonNightMinFeelsLikeTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#feels-like-min-night" } +Number:Temperature ForecastLondonDayMaxScreenTemperature (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#temp-max-day" } +Number:Temperature ForecastLondonNightMinScreenTemperature (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#temp-min-night" } +Number:Temperature ForecastLondonDayUpperBoundMaxFeelsLikeTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#feels-like-max-ub-day" } +Number:Temperature ForecastLondonNightUpperBoundMinFeelsLikeTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#feels-like-min-ub-night" } +Number:Temperature ForecastLondonDayLowerBoundMaxFeelsLikeTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#feels-like-max-lb-day" } +Number:Temperature ForecastLondonNightLowerBoundMinFeelsLikeTemp (gLondonCurrentDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#feels-like-min-lb-night" } +Number:Dimensionless ForecastLondonDayProbabilityOfPrecipitation (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#precip-prob-day" } +Number:Dimensionless ForecastLondonNightProbabilityOfPrecipitation (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#precip-prob-night" } +Number:Dimensionless ForecastLondonDayProbabilityOfSnow (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#snow-prob-day" } +Number:Dimensionless ForecastLondonNightProbabilityOfSnow (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#snow-prob-night" } +Number:Dimensionless ForecastLondonDayProbabilityOfHeavySnow (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#heavy-snow-prob-day" } +Number:Dimensionless ForecastLondonNightProbabilityOfHeavySnow (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#heavy-snow-prob-night" } +Number:Dimensionless ForecastLondonDayProbabilityOfRain (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#rain-prob-day" } +Number:Dimensionless ForecastLondonNightProbabilityOfRain (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#rain-prob-night" } +Number:Dimensionless ForecastLondonDayProbabilityOfHeavyRain (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#day-prob-heavy-rain" } +Number:Dimensionless ForecastLondonNightProbabilityOfHeavyRain (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#night-prob-heavy-rain" } +Number:Dimensionless ForecastLondonDayProbabilityOfHail (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#hail-prob-day" } +Number:Dimensionless ForecastLondonNightProbabilityOfHail (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#hail-prob-night" } +Number:Dimensionless ForecastLondonDayProbabilityOfSferics (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#sferics-prob-day" } +Number:Dimensionless ForecastLondonNightProbabilityOfSferics (gLondonCurrentDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast#sferics-prob-night" } + +Group gCurrentDailyPlus01Forecast "Current Day +1 Daily Forecast" +Group gLondonNextDay "London Next Day Forecast" (gLondon,gCurrentDailyPlus01Forecast) +DateTime ForecastLondonPlus01DailyForecastTs (gLondonNextDay) { channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#forecast-ts" } +Number:Speed ForecastLondonPlus01MiddayWindSpeed10m (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-speed-day" } +Number:Speed ForecastLondonPlus01MidnightWindSpeed10m (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-speed-night" } +Number:Angle ForecastLondonPlus01Midday10MWindDirection (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-direction-day" } +Number:Angle ForecastLondonPlus01Midnight10MWindDirection (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-direction-night" } +Number:Speed ForecastLondonPlus01Midday10mWindGust (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-gust-day" } +Number:Speed ForecastLondonPlus01Midnight10mWindGust (gLondonNextDay) { unit="m/s",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#wind-gust-night" } +Number:Length ForecastLondonPlus01MiddayVisibility (gLondonNextDay) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#visibility-day" } +Number:Length ForecastLondonPlus01MidnightVisibility (gLondonNextDay) { unit="m",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#visibility-night" } +Number:Dimensionless ForecastLondonPlus01MiddayRelativeHumidity (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#humidity-day" } +Number:Dimensionless ForecastLondonPlus01MidnightRelativeHumidity (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#humidity-night" } +Number:Pressure ForecastLondonPlus01Middaypressure (gLondonNextDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#pressure-day" } +Number:Pressure ForecastLondonPlus01Midnightpressure (gLondonNextDay) { unit="Pa",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#pressure-night" } +Number:Dimensionless ForecastLondonPlus01MaxUvIndex (gLondonNextDay) { channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#uv-max" } +Number:Temperature ForecastLondonPlus01NightUpperBoundMinTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#temp-min-ub-night" } +Number:Temperature ForecastLondonPlus01DayLowerBoundMaxTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#temp-max-lb-day" } +Number:Temperature ForecastLondonPlus01NightLowerBoundMinTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#temp-min-lb-night" } +Number:Temperature ForecastLondonPlus01DayMaxFeelsLikeTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#feels-like-max-day" } +Number:Temperature ForecastLondonPlus01NightMinFeelsLikeTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#feels-like-min-night" } +Number:Temperature ForecastLondonPlus01DayMaxScreenTemperature (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#temp-max-day" } +Number:Temperature ForecastLondonPlus01NightMinScreenTemperature (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#temp-min-night" } +Number:Temperature ForecastLondonPlus01DayUpperBoundMaxFeelsLikeTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#feels-like-max-ub-day" } +Number:Temperature ForecastLondonPlus01NightUpperBoundMinFeelsLikeTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#feels-like-min-ub-night" } +Number:Temperature ForecastLondonPlus01DayLowerBoundMaxFeelsLikeTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#feels-like-max-lb-day" } +Number:Temperature ForecastLondonPlus01NightLowerBoundMinFeelsLikeTemp (gLondonNextDay) { unit="°C",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#feels-like-min-lb-night" } +Number:Dimensionless ForecastLondonPlus01DayProbabilityOfPrecipitation (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#precip-prob-day" } +Number:Dimensionless ForecastLondonPlus01NightProbabilityOfPrecipitation (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#precip-prob-night" } +Number:Dimensionless ForecastLondonPlus01DayProbabilityOfSnow (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#snow-prob-day" } +Number:Dimensionless ForecastLondonPlus01NightProbabilityOfSnow (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#snow-prob-night" } +Number:Dimensionless ForecastLondonPlus01DayProbabilityOfHeavySnow (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#heavy-snow-prob-day" } +Number:Dimensionless ForecastLondonPlus01NightProbabilityOfHeavySnow (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#heavy-snow-prob-night" } +Number:Dimensionless ForecastLondonPlus01DayProbabilityOfRain (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#rain-prob-day" } +Number:Dimensionless ForecastLondonPlus01NightProbabilityOfRain (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#rain-prob-night" } +Number:Dimensionless ForecastLondonPlus01DayProbabilityOfHeavyRain (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#day-prob-heavy-rain" } +Number:Dimensionless ForecastLondonPlus01NightProbabilityOfHeavyRain (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#night-prob-heavy-rain" } +Number:Dimensionless ForecastLondonPlus01DayProbabilityOfHail (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#hail-prob-day" } +Number:Dimensionless ForecastLondonPlus01NightProbabilityOfHail (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#hail-prob-night" } +Number:Dimensionless ForecastLondonPlus01DayProbabilityOfSferics (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#sferics-prob-day" } +Number:Dimensionless ForecastLondonPlus01NightProbabilityOfSferics (gLondonNextDay) { unit="%",channel="metofficedatahub:site:metoffice:londonForecast:daily-forecast-plus01#sferics-prob-night" } +``` + +### Configuration (*.sitemap) + +#### Hourly Forecast `example.sitemap` + +```perl +Frame { + Text item=ForecastLondonHourlyForecastTs icon="time" + Text item=ForecastLondonCurrentHour icon="temperature" + Text item=ForecastLondonMinTemp icon="temperature" + Text item=ForecastLondonMaxTemp icon="temperature" + Text item=ForecastLondonFeelsLikeTemp icon="temperature" + Text item=ForecastLondonRelHumidity icon="humidity" + Text item=ForecastLondonVisibility icon="sun_clouds" + Text item=ForecastLondonPrecipitationRate icon="rain" + Text item=ForecastLondonPrecipitationProb icon="rain" + Text item=ForecastLondonPrecipitationAmount icon="rain" + Text item=ForecastLondonSnowAmount icon="rain" + Text item=ForecastLondonUvIndex icon="sun" + Text item=ForecastLondonpressure icon="pressure" + Text item=ForecastLondon10mWindSpeed icon="wind" + Text item=ForecastLondon10mGustWindSpeed icon="wind" + Text item=ForecastLondon10mMaxGustWindSpeed icon="wind" + Text item=ForecastLondon10mWindDirection icon="wind" + Text item=ForecastLondonDewPointTemp icon="temperature" +} + +Frame { + Text item=ForecastLondonPlus01HourlyForecastTs icon="time" + Text item=ForecastLondonPlus01CurrentHour icon="temperature" + Text item=ForecastLondonPlus01MinTemp icon="temperature" + Text item=ForecastLondonPlus01MaxTemp icon="temperature" + Text item=ForecastLondonPlus01FeelsLikeTemp icon="temperature" + Text item=ForecastLondonPlus01RelHumidity icon="humidity" + Text item=ForecastLondonPlus01Visibility icon="sun_clouds" + Text item=ForecastLondonPlus01PrecipitationRate icon="rain" + Text item=ForecastLondonPlus01PrecipitationProb icon="rain" + Text item=ForecastLondonPlus01PrecipitationAmount icon="rain" + Text item=ForecastLondonPlus01SnowAmount icon="rain" + Text item=ForecastLondonPlus01UvIndex icon="sun" + Text item=ForecastLondonPlus01pressure icon="pressure" + Text item=ForecastLondonPlus0110mWindSpeed icon="wind" + Text item=ForecastLondonPlus0110mGustWindSpeed icon="wind" + Text item=ForecastLondonPlus0110mMaxGustWindSpeed icon="wind" + Text item=ForecastLondonPlus0110mWindDirection icon="wind" + Text item=ForecastLondonPlus01DewPointTemp icon="temperature" +} +``` + +#### Daily Forecast `example.items` + +```perl +Frame { + Text item=ForecastLondonDailyForecastTs icon="time" + Text item=ForecastLondonMiddayWindSpeed10m icon="wind" + Text item=ForecastLondonMidnightWindSpeed10m icon="wind" + Text item=ForecastLondonMidday10MWindDirection icon="wind" + Text item=ForecastLondonMidnight10MWindDirection icon="wind" + Text item=ForecastLondonMidday10mWindGust icon="wind" + Text item=ForecastLondonMidnight10mWindGust icon="wind" + Text item=ForecastLondonMiddayVisibility icon="sun_clouds" + Text item=ForecastLondonMidnightVisibility icon="sun_clouds" + Text item=ForecastLondonMiddayRelativeHumidity icon="humidity" + Text item=ForecastLondonMidnightRelativeHumidity icon="humidity" + Text item=ForecastLondonMiddaypressure icon="pressure" + Text item=ForecastLondonMidnightpressure icon="pressure" + Text item=ForecastLondonMaxUvIndex icon="pressure" + Text item=ForecastLondonNightUpperBoundMinTemp icon="temperature" + Text item=ForecastLondonDayLowerBoundMaxTemp icon="temperature" + Text item=ForecastLondonNightLowerBoundMinTemp icon="temperature" + Text item=ForecastLondonDayMaxFeelsLikeTemp icon="temperature" + Text item=ForecastLondonNightMinFeelsLikeTemp icon="temperature" + Text item=ForecastLondonDayMaxScreenTemperature icon="temperature" + Text item=ForecastLondonNightMinScreenTemperature icon="temperature" + Text item=ForecastLondonDayUpperBoundMaxFeelsLikeTemp icon="temperature" + Text item=ForecastLondonNightUpperBoundMinFeelsLikeTemp icon="temperature" + Text item=ForecastLondonDayLowerBoundMaxFeelsLikeTemp icon="temperature" + Text item=ForecastLondonNightLowerBoundMinFeelsLikeTemp icon="temperature" + Text item=ForecastLondonDayProbabilityOfPrecipitation icon="rain" + Text item=ForecastLondonNightProbabilityOfPrecipitation icon="rain" + Text item=ForecastLondonDayProbabilityOfSnow icon="rain" + Text item=ForecastLondonNightProbabilityOfSnow icon="rain" + Text item=ForecastLondonDayProbabilityOfHeavySnow icon="rain" + Text item=ForecastLondonNightProbabilityOfHeavySnow icon="rain" + Text item=ForecastLondonDayProbabilityOfRain icon="rain" + Text item=ForecastLondonNightProbabilityOfRain icon="rain" + Text item=ForecastLondonDayProbabilityOfHeavyRain icon="rain" + Text item=ForecastLondonNightProbabilityOfHeavyRain icon="rain" + Text item=ForecastLondonDayProbabilityOfHail icon="rain" + Text item=ForecastLondonNightProbabilityOfHail icon="rain" + Text item=ForecastLondonDayProbabilityOfSferics icon="line" + Text item=ForecastLondonNightProbabilityOfSferics icon="line" +} + +Frame { + Text item=ForecastLondonPlus01DailyForecastTs icon="time" + Text item=ForecastLondonPlus01MiddayWindSpeed10m icon="wind" + Text item=ForecastLondonPlus01MidnightWindSpeed10m icon="wind" + Text item=ForecastLondonPlus01Midday10MWindDirection icon="wind" + Text item=ForecastLondonPlus01Midnight10MWindDirection icon="wind" + Text item=ForecastLondonPlus01Midday10mWindGust icon="wind" + Text item=ForecastLondonPlus01Midnight10mWindGust icon="wind" + Text item=ForecastLondonPlus01MiddayVisibility icon="sun_clouds" + Text item=ForecastLondonPlus01MidnightVisibility icon="sun_clouds" + Text item=ForecastLondonPlus01MiddayRelativeHumidity icon="humidity" + Text item=ForecastLondonPlus01MidnightRelativeHumidity icon="humidity" + Text item=ForecastLondonPlus01Middaypressure icon="pressure" + Text item=ForecastLondonPlus01Midnightpressure icon="pressure" + Text item=ForecastLondonPlus01MaxUvIndex icon="pressure" + Text item=ForecastLondonPlus01NightUpperBoundMinTemp icon="temperature" + Text item=ForecastLondonPlus01DayLowerBoundMaxTemp icon="temperature" + Text item=ForecastLondonPlus01NightLowerBoundMinTemp icon="temperature" + Text item=ForecastLondonPlus01DayMaxFeelsLikeTemp icon="temperature" + Text item=ForecastLondonPlus01NightMinFeelsLikeTemp icon="temperature" + Text item=ForecastLondonPlus01DayMaxScreenTemperature icon="temperature" + Text item=ForecastLondonPlus01NightMinScreenTemperature icon="temperature" + Text item=ForecastLondonPlus01DayUpperBoundMaxFeelsLikeTemp icon="temperature" + Text item=ForecastLondonPlus01NightUpperBoundMinFeelsLikeTemp icon="temperature" + Text item=ForecastLondonPlus01DayLowerBoundMaxFeelsLikeTemp icon="temperature" + Text item=ForecastLondonPlus01NightLowerBoundMinFeelsLikeTemp icon="temperature" + Text item=ForecastLondonPlus01DayProbabilityOfPrecipitation icon="rain" + Text item=ForecastLondonPlus01NightProbabilityOfPrecipitation icon="rain" + Text item=ForecastLondonPlus01DayProbabilityOfSnow icon="rain" + Text item=ForecastLondonPlus01NightProbabilityOfSnow icon="rain" + Text item=ForecastLondonPlus01DayProbabilityOfHeavySnow icon="rain" + Text item=ForecastLondonPlus01NightProbabilityOfHeavySnow icon="rain" + Text item=ForecastLondonPlus01DayProbabilityOfRain icon="rain" + Text item=ForecastLondonPlus01NightProbabilityOfRain icon="rain" + Text item=ForecastLondonPlus01DayProbabilityOfHeavyRain icon="rain" + Text item=ForecastLondonPlus01NightProbabilityOfHeavyRain icon="rain" + Text item=ForecastLondonPlus01DayProbabilityOfHail icon="rain" + Text item=ForecastLondonPlus01NightProbabilityOfHail icon="rain" + Text item=ForecastLondonPlus01DayProbabilityOfSferics icon="line" + Text item=ForecastLondonPlus01NightProbabilityOfSferics icon="line" +} +``` diff --git a/bundles/org.openhab.binding.metofficedatahub/pom.xml b/bundles/org.openhab.binding.metofficedatahub/pom.xml new file mode 100644 index 0000000000000..a4f22a3a00811 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 4.3.0-SNAPSHOT + + + org.openhab.binding.metofficedatahub + + openHAB Add-ons :: Bundles :: MetOffice DataHub Binding + + diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/feature/feature.xml b/bundles/org.openhab.binding.metofficedatahub/src/main/feature/feature.xml new file mode 100644 index 0000000000000..bac4fe9eb52c0 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.metofficedatahub/${project.version} + + diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBindingConstants.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBindingConstants.java new file mode 100644 index 0000000000000..8eb5a4a4793d4 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBindingConstants.java @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal; + +import java.util.Random; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +import com.google.gson.FieldNamingPolicy; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +/** + * The {@link MetOfficeDataHubBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class MetOfficeDataHubBindingConstants { + + public static final Gson GSON = new GsonBuilder() + .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).setPrettyPrinting() + .disableHtmlEscaping().serializeNulls().create(); + + private static final String BINDING_ID = "metofficedatahub"; + + // List of all Thing Type UIDs + public static final ThingTypeUID THING_TYPE_BRIDGE = new ThingTypeUID(BINDING_ID, "account"); + public static final ThingTypeUID THING_TYPE_SITE_SPEC_API = new ThingTypeUID(BINDING_ID, "site"); + + /** + * Site Specific API - Shared + */ + + public static final String SITE_TIMESTAMP = "forecast-ts"; + + /** + * Site Specific API - Hourly Forecast Channel Names + */ + public static final String SITE_HOURLY_FORECAST_SCREEN_TEMPERATURE = "air-temp-current"; + + public static final String SITE_HOURLY_FORECAST_MIN_SCREEN_TEMPERATURE = "air-temp-min"; + public static final String SITE_HOURLY_FORECAST_MAX_SCREEN_TEMPERATURE = "air-temp-max"; + + public static final String SITE_HOURLY_FEELS_LIKE_TEMPERATURE = "feels-like"; + + public static final String SITE_HOURLY_SCREEN_RELATIVE_HUMIDITY = "humidity"; + + public static final String SITE_HOURLY_VISIBILITY = "visibility"; + + public static final String SITE_HOURLY_PROBABILITY_OF_PRECIPITATION = "precip-prob"; + + public static final String SITE_HOURLY_PRECIPITATION_RATE = "precip-rate"; + + public static final String SITE_HOURLY_TOTAL_PRECIPITATION_AMOUNT = "precip-total"; + + public static final String SITE_HOURLY_TOTAL_SNOW_AMOUNT = "snow-total"; + + public static final String SITE_HOURLY_UV_INDEX = "uv-index"; + + public static final String SITE_HOURLY_PRESSURE = "pressure"; + + public static final String SITE_HOURLY_WIND_SPEED_10M = "wind-speed"; + + public static final String SITE_HOURLY_WIND_GUST_SPEED_10M = "wind-speed-gust"; + + public static final String SITE_HOURLY_MAX_10M_WIND_GUST = "wind-gust-max"; + + public static final String SITE_HOURLY_WIND_DIRECTION_FROM_10M = "wind-direction"; + + public static final String SITE_HOURLY_SCREEN_DEW_POINT_TEMPERATURE = "dewpoint"; + + public static final String SITE_DAILY_MIDDAY_WIND_SPEED_10M = "wind-speed-day"; + + public static final String SITE_DAILY_MIDNIGHT_WIND_SPEED_10M = "wind-speed-night"; + + public static final String SITE_DAILY_MIDDAY_WIND_DIRECTION_10M = "wind-direction-day"; + public static final String SITE_DAILY_MIDNIGHT_WIND_DIRECTION_10M = "wind-direction-night"; + + public static final String SITE_DAILY_MIDDAY_WIND_GUST_10M = "wind-gust-day"; + + public static final String SITE_DAILY_MIDNIGHT_WIND_GUST_10M = "wind-gust-night"; + + public static final String SITE_DAILY_MIDDAY_VISIBILITY = "visibility-day"; + + public static final String SITE_DAILY_MIDNIGHT_VISIBILITY = "visibility-night"; + + public static final String SITE_DAILY_MIDDAY_REL_HUMIDITY = "humidity-day"; + + public static final String SITE_DAILY_MIDNIGHT_REL_HUMIDITY = "humidity-night"; + + public static final String SITE_DAILY_MIDDAY_PRESSURE = "pressure-day"; + public static final String SITE_DAILY_MIDNIGHT_PRESSURE = "pressure-night"; + + public static final String SITE_DAILY_DAY_MAX_UV_INDEX = "uv-max"; + + public static final String SITE_DAILY_DAY_UPPER_BOUND_MAX_TEMP = "temp-max-ub-day"; + public static final String SITE_DAILY_DAY_LOWER_BOUND_MAX_TEMP = "temp-max-lb-day"; + + public static final String SITE_DAILY_NIGHT_UPPER_BOUND_MAX_TEMP = "temp-min-ub-night"; + public static final String SITE_DAILY_NIGHT_LOWER_BOUND_MAX_TEMP = "temp-min-lb-night"; + + public static final String SITE_DAILY_NIGHT_FEELS_LIKE_MIN_TEMP = "feels-like-min-night"; + + public static final String SITE_DAILY_DAY_FEELS_LIKE_MAX_TEMP = "feels-like-max-day"; + + public static final String SITE_DAILY_NIGHT_LOWER_BOUND_MIN_TEMP = "temp-min-lb-night"; + + public static final String SITE_DAILY_DAY_MAX_FEELS_LIKE_TEMP = "feels-like-max-day"; + + public static final String SITE_DAILY_NIGHT_LOWER_BOUND_MIN_FEELS_LIKE_TEMP = "feels-like-min-lb-night"; + + public static final String SITE_DAILY_DAY_LOWER_BOUND_MAX_FEELS_LIKE_TEMP = "feels-like-max-lb-day"; + + public static final String SITE_DAILY_DAY_UPPER_BOUND_MAX_FEELS_LIKE_TEMP = "feels-like-max-ub-day"; + + public static final String SITE_DAILY_UPPER_BOUND_MIN_FEELS_LIKE_TEMP = "feels-like-min-ub-night"; + + public static final String SITE_DAILY_DAY_PROBABILITY_OF_PRECIPITATION = "precip-prob-day"; + + public static final String SITE_DAILY_NIGHT_PROBABILITY_OF_PRECIPITATION = "precip-prob-night"; + + public static final String SITE_DAILY_DAY_PROBABILITY_OF_SNOW = "snow-prob-day"; + + public static final String SITE_DAILY_NIGHT_PROBABILITY_OF_SNOW = "snow-prob-night"; + + public static final String SITE_DAILY_DAY_PROBABILITY_OF_HEAVY_SNOW = "heavy-snow-prob-day"; + + public static final String SITE_DAILY_NIGHT_PROBABILITY_OF_HEAVY_SNOW = "heavy-snow-prob-night"; + + public static final String SITE_DAILY_DAY_PROBABILITY_OF_RAIN = "rain-prob-day"; + + public static final String SITE_DAILY_NIGHT_PROBABILITY_OF_RAIN = "rain-prob-night"; + + public static final String SITE_DAILY_DAY_PROBABILITY_OF_HEAVY_RAIN = "day-prob-heavy-rain"; + + public static final String SITE_DAILY_NIGHT_PROBABILITY_OF_HEAVY_RAIN = "night-prob-heavy-rain"; + + public static final String SITE_DAILY_DAY_PROBABILITY_OF_HAIL = "hail-prob-day"; + + public static final String SITE_DAILY_NIGHT_PROBABILITY_OF_HAIL = "hail-prob-night"; + + public static final String SITE_DAILY_DAY_PROBABILITY_OF_SFERICS = "sferics-prob-day"; + + public static final String SITE_DAILY_NIGHT_PROBABILITY_OF_SFERICS = "sferics-prob-night"; + + public static final String SITE_DAILY_DAY_MAX_SCREEN_TEMPERATURE = "temp-max-day"; + public static final String SITE_DAILY_NIGHT_MIN_SCREEN_TEMPERATURE = "temp-min-night"; + + public static final String GROUP_PREFIX_HOURS_FORECAST = "current-forecast"; + public static final String GROUP_PREFIX_DAILY_FORECAST = "daily-forecast"; + public static final String GROUP_POSTFIX_BOTH_FORECASTS = "-plus"; + public static final char GROUP_PREFIX_TO_ITEM = '#'; + + public static final String GET_FORECAST_URL_DAILY = "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/daily?latitude=&longitude="; + public static final String GET_FORECAST_URL_HOURLY = "https://data.hub.api.metoffice.gov.uk/sitespecific/v0/point/hourly?latitude=&longitude="; + public static final String GET_FORECAST_KEY_LATITUDE = ""; + public static final String GET_FORECAST_KEY_LONGITUDE = ""; + public static final String GET_FORECAST_API_KEY_HEADER = "apikey"; + public static final int GET_FORECAST_REQUEST_TIMEOUT_SECONDS = 3; + public static final String EXPECTED_TS_FORMAT = "YYYY-MM-dd HH:mm:ss.SSS"; + + public static final long DAY_IN_MILLIS = 86400000; + + public static final Random RANDOM_GENERATOR = new Random(); + + public static final String BRIDGE_PROP_FORECAST_REQUEST_COUNT = "Site Specific API Call Count"; + + public static final Runnable NO_OP = () -> { + }; +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBridgeConfiguration.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBridgeConfiguration.java new file mode 100644 index 0000000000000..0949be7a4f8cf --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBridgeConfiguration.java @@ -0,0 +1,35 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MetOfficeDataHubBridgeConfiguration} class contains fields mapping thing configuration parameters. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class MetOfficeDataHubBridgeConfiguration { + + /** + * Site Specific API Subscription - API Key + */ + public String siteApiKey = ""; + + /** + * Rate limit of API call's in 24 hour period starting from 0000 (Free is capped at 360 - this allows 110 due to + * reboots) + */ + public int siteRateDailyLimit = 250; +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBridgeHandler.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBridgeHandler.java new file mode 100644 index 0000000000000..573ef896a1635 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubBridgeHandler.java @@ -0,0 +1,159 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal; + +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.BRIDGE_PROP_FORECAST_REQUEST_COUNT; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.metofficedatahub.internal.api.IConnectionStatusListener; +import org.openhab.binding.metofficedatahub.internal.api.IRateLimiterListener; +import org.openhab.binding.metofficedatahub.internal.api.RequestLimiter; +import org.openhab.binding.metofficedatahub.internal.api.SiteApi; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +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.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link MetOfficeDataHubBridgeHandler} models the account(s) to the MetOfficeDataHub services. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class MetOfficeDataHubBridgeHandler extends BaseBridgeHandler + implements IRateLimiterListener, IConnectionStatusListener { + + private volatile MetOfficeDataHubBridgeConfiguration config = getConfigAs( + MetOfficeDataHubBridgeConfiguration.class); + + private final TranslationProvider translationProvider; + private final LocaleProvider localeProvider; + private final Bundle bundle; + + private SiteApi siteApi; + private String bridgeId = ""; + + public MetOfficeDataHubBridgeHandler(final Bridge bridge, @Reference HttpClientFactory httpClientFactory, + @Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider, + @Reference StorageService storageService, @Reference TimeZoneProvider timeZoneProvider) { + super(bridge); + bridgeId = getThing().getUID().getAsString(); + this.translationProvider = translationProvider; + this.localeProvider = localeProvider; + this.bundle = FrameworkUtil.getBundle(getClass()); + this.siteApi = new SiteApi(bridgeId, httpClientFactory, storageService, translationProvider, localeProvider, + timeZoneProvider, scheduler); + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + + siteApi.registerListeners(bridgeId, this); + + config = getConfigAs(MetOfficeDataHubBridgeConfiguration.class); + + siteApi.setLimits(config.siteRateDailyLimit); + siteApi.setApiKey(config.siteApiKey); + siteApi.validateSiteApi(); + } + + @Override + public void dispose() { + siteApi.deregisterListeners(bridgeId, this); + siteApi.dispose(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + // API Management + + public SiteApi getSiteApi() { + return siteApi; + } + + // Localization functionality + + public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) { + String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments); + return Objects.nonNull(result) ? result : key; + } + + // Implementation of IRateLimiterListener + + @Override + public void processRateLimiterUpdated(RequestLimiter requestLimiter) { + final Map newProps = new HashMap<>(); + newProps.put(BRIDGE_PROP_FORECAST_REQUEST_COUNT, String.valueOf(requestLimiter.getCurrentRequestCount())); + this.updateProperties(newProps); + } + + // Implementation of IConnectionStatusListener + + @Override + public void processAuthenticationResult(boolean authenticated) { + if (!authenticated) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + getLocalizedText("bridge.error.site-specific.auth-issue")); + } else { + processConnected(); + } + } + + @Override + public void processCommunicationFailure(final @Nullable Throwable e) { + String message = ""; + if (e != null) { + if (e.getLocalizedMessage() != null) { + message = e.getLocalizedMessage(); + } else if (e.getMessage() != null) { + message = e.getMessage(); + } + } + if (message == null || message.isBlank()) { + message = getLocalizedText("bridge.error.site-specific.communication-failure.unknown"); + } + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + getLocalizedText("bridge.error.site-specific.communication-failure", message)); + } + + @Override + public void processConnected() { + updateStatus(ThingStatus.ONLINE); + for (Thing thing : getThing().getThings()) { + if (thing instanceof MetOfficeDataHubSiteHandler siteHandler) { + if (!siteHandler.requiresPoll()) { + siteHandler.scheduleInitTask(); + } + } + } + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubHandlerFactory.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubHandlerFactory.java new file mode 100644 index 0000000000000..76c098251f350 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubHandlerFactory.java @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal; + +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.*; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.storage.StorageService; +import org.openhab.core.thing.Bridge; +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.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link MetOfficeDataHubHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.metofficedatahub", service = ThingHandlerFactory.class) +public class MetOfficeDataHubHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_BRIDGE, + THING_TYPE_SITE_SPEC_API); + + private final LocationProvider locationProvider; + private final HttpClientFactory httpClientFactory; + private final TranslationProvider translationProvider; + private final LocaleProvider localeProvider; + private final TimeZoneProvider timeZoneProvider; + private final StorageService storageService; + + @Activate + public MetOfficeDataHubHandlerFactory(@Reference LocationProvider locationProvider, + @Reference HttpClientFactory httpClientFactory, @Reference TranslationProvider translationProvider, + @Reference LocaleProvider localeProvider, @Reference TimeZoneProvider timeZoneProvider, + @Reference StorageService storageService) { + this.locationProvider = locationProvider; + this.httpClientFactory = httpClientFactory; + this.translationProvider = translationProvider; + this.localeProvider = localeProvider; + this.timeZoneProvider = timeZoneProvider; + this.storageService = storageService; + } + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_BRIDGE.equals(thingTypeUID)) { + return new MetOfficeDataHubBridgeHandler((Bridge) thing, httpClientFactory, translationProvider, + localeProvider, storageService, timeZoneProvider); + } else if (THING_TYPE_SITE_SPEC_API.equals(thingTypeUID)) { + return new MetOfficeDataHubSiteHandler(thing, locationProvider, translationProvider, localeProvider, + timeZoneProvider); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubSiteConfiguration.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubSiteConfiguration.java new file mode 100644 index 0000000000000..cae23315c4274 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubSiteConfiguration.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link MetOfficeDataHubSiteConfiguration} class contains fields mapping thing configuration parameters. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class MetOfficeDataHubSiteConfiguration { + + public String location = ""; + + public int hourlyForecastPollRate = 1; + + public int dailyForecastPollRate = 3; +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubSiteHandler.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubSiteHandler.java new file mode 100644 index 0000000000000..0ed30b8fae373 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDataHubSiteHandler.java @@ -0,0 +1,577 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal; + +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.*; +import static org.openhab.core.library.unit.MetricPrefix.MILLI; +import static org.openhab.core.library.unit.SIUnits.METRE; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import javax.measure.Unit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.metofficedatahub.internal.api.ISiteResponseListener; +import org.openhab.binding.metofficedatahub.internal.api.ResponseDataProcessor; +import org.openhab.binding.metofficedatahub.internal.dto.responses.SiteApiFeatureCollection; +import org.openhab.binding.metofficedatahub.internal.dto.responses.SiteApiFeatureProperties; +import org.openhab.binding.metofficedatahub.internal.dto.responses.SiteApiTimeSeries; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.LocationProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.library.types.DateTimeType; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.PointType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.thing.Bridge; +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.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.openhab.core.types.UnDefType; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Reference; + +/** + * The {@link MetOfficeDataHubSiteHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class MetOfficeDataHubSiteHandler extends BaseThingHandler implements ISiteResponseListener { + + private final Object checkDataRequiredSchedulerLock = new Object(); + private final Object checkDailySchedulerLock = new Object(); + private final TranslationProvider translationProvider; + private final LocaleProvider localeProvider; + private final Bundle bundle; + private final LocationProvider locationProvider; + private final PollManager dailyForecastPollManager; + private final PollManager hourlyForecastPollManager; + + private volatile MetOfficeDataHubSiteConfiguration config = getConfigAs(MetOfficeDataHubSiteConfiguration.class); + + private PointType location = new PointType(); + private @Nullable ScheduledFuture checkDataRequiredScheduler = null; + private @Nullable ScheduledFuture dailyScheduler = null; + private @Nullable ScheduledFuture initTask = null; + + private String dailyPollKey = ""; + private String hourlyPollKey = ""; + + public MetOfficeDataHubSiteHandler(Thing thing, @Reference LocationProvider locationProvider, + @Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider, + @Reference TimeZoneProvider timeZoneProvider) { + super(thing); + this.locationProvider = locationProvider; + this.translationProvider = translationProvider; + this.localeProvider = localeProvider; + this.bundle = FrameworkUtil.getBundle(getClass()); + + final ResponseDataProcessor updateHourlyFromCache = new ResponseDataProcessor() { + @Override + public void processResponse(final String content) { + processHourlyContent(content); + } + }; + + final ResponseDataProcessor updateDailyFromCache = new ResponseDataProcessor() { + @Override + public void processResponse(final String content) { + processDailyContent(content); + } + }; + + this.hourlyForecastPollManager = new PollManager("Hourly", timeZoneProvider, scheduler, Duration.ofHours(1), + updateHourlyFromCache, () -> { + sendForecastRequest(false); + }); + this.dailyForecastPollManager = new PollManager("Daily", timeZoneProvider, scheduler, Duration.ofHours(3), + updateDailyFromCache, () -> { + sendForecastRequest(true); + }); + } + + @Override + public void dispose() { + cancelInitTask(); + cancelDataRequiredCheck(); + cancelScheduleDailyDataPoll(true); + hourlyForecastPollManager.dispose(); + dailyForecastPollManager.dispose(); + super.dispose(); + } + + @Override + public void initialize() { + updateStatus(ThingStatus.UNKNOWN); + dailyForecastPollManager.setDataRequired(false, false); + hourlyForecastPollManager.setDataRequired(false, false); + + config = getConfigAs(MetOfficeDataHubSiteConfiguration.class); + + if (config.location.isBlank()) { + @Nullable + PointType userLocation = locationProvider.getLocation(); + if (userLocation == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + getLocalizedText("site.error.no-user-location")); + return; + } else { + location = userLocation; + } + } else { + try { + location = new PointType(config.location); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + getLocalizedText("site.error.invalid-location")); + return; + } + } + + dailyPollKey = location + ",daily"; + hourlyPollKey = location + ",hourly"; + + if (config.hourlyForecastPollRate > 0) { + hourlyForecastPollManager.setPollDuration(Duration.ofHours(config.hourlyForecastPollRate)); + } + if (config.dailyForecastPollRate > 0) { + dailyForecastPollManager.setPollDuration(Duration.ofHours(config.dailyForecastPollRate)); + } + + scheduleInitTask(); + } + + @Override + public void channelLinked(ChannelUID channelUID) { + super.channelLinked(channelUID); + + handleCommand(channelUID, RefreshType.REFRESH); + } + + @Override + public void channelUnlinked(ChannelUID channelUID) { + // can be overridden by subclasses + scheduleDataRequiredCheck(); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + scheduler.execute(() -> { + if (RefreshType.REFRESH.equals(command)) { + scheduleDataRequiredCheck(); + } + }); + } + + private @Nullable MetOfficeDataHubBridgeHandler getMetOfficeDataHubBridge() { + Bridge baseBridge = getBridge(); + + if (baseBridge != null && baseBridge.getHandler() instanceof MetOfficeDataHubBridgeHandler bridgeHandler) { + return bridgeHandler; + } else { + return null; + } + } + + protected boolean requiresPoll() { + return hourlyForecastPollManager.getIsDataRequired() || dailyForecastPollManager.getIsDataRequired(); + } + + protected void scheduleInitTask() { + cancelInitTask(); + initTask = scheduler.schedule(() -> { + MetOfficeDataHubBridgeHandler metBridge = getMetOfficeDataHubBridge(); + if (metBridge != null) { + updateStatus(metBridge.getThing().getStatus()); + } + + checkDataRequired(); + }, 200, TimeUnit.MILLISECONDS); + } + + private void cancelInitTask() { + final ScheduledFuture initTaskRef = initTask; + if (initTaskRef != null) { + initTaskRef.cancel(true); + initTask = null; + } + } + + private void checkDataRequired() { + final List<@Nullable String> activeGroups = getThing().getChannels().stream().filter(x -> isLinked(x.getUID())) + .map(x -> x.getUID().getGroupId()).distinct().toList(); + + if (activeGroups.stream().anyMatch(g -> g != null && g.startsWith(GROUP_PREFIX_DAILY_FORECAST))) { + dailyForecastPollManager.setDataRequired(true, true); + } else { + dailyForecastPollManager.setDataRequired(false, false); + } + + if (activeGroups.stream().anyMatch(g -> g != null && g.startsWith(GROUP_PREFIX_HOURS_FORECAST))) { + hourlyForecastPollManager.setDataRequired(true, true); + } else { + hourlyForecastPollManager.setDataRequired(false, false); + } + } + + private void sendForecastRequest(final boolean daily) { + MetOfficeDataHubBridgeHandler uplinkBridge = getMetOfficeDataHubBridge(); + if (uplinkBridge == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + getLocalizedText("site.error.no-bridge")); + return; + } + final String pollId = (daily) ? dailyPollKey : hourlyPollKey; + final MetOfficeDataHubBridgeHandler metOfficeBridgeHandler = getMetOfficeDataHubBridge(); + if (metOfficeBridgeHandler != null) { + if (!metOfficeBridgeHandler.getSiteApi().sendRequest(daily, location, this, pollId)) { + if (daily) { + dailyForecastPollManager.cachedPollOrLiveStart(false); + } else { + hourlyForecastPollManager.cachedPollOrLiveStart(false); + } + } + } + } + + /** + * Scheduler to evaluate which data is required to be polled from + * the APIs + */ + private void scheduleDataRequiredCheck() { + synchronized (checkDataRequiredSchedulerLock) { + cancelDataRequiredCheck(); + checkDataRequiredScheduler = scheduler.schedule(this::checkDataRequired, 2, TimeUnit.SECONDS); + } + } + + private void cancelDataRequiredCheck() { + synchronized (checkDataRequiredSchedulerLock) { + ScheduledFuture job = checkDataRequiredScheduler; + if (job != null) { + job.cancel(true); + checkDataRequiredScheduler = null; + } + } + } + + private void cancelScheduleDailyDataPoll(final boolean allowInterrupt) { + synchronized (checkDailySchedulerLock) { + ScheduledFuture job = dailyScheduler; + if (job != null) { + job.cancel(allowInterrupt); + dailyScheduler = null; + } + } + } + + // Localization functionality + + public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) { + String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments); + return Objects.nonNull(result) ? result : key; + } + + // Implementation of ISiteResponseListener and associated methods + + @Override + public void processDailyResponse(final String responseData, final String pollId) { + if (dailyPollKey.equals(pollId)) { + dailyForecastPollManager.setDataContentReceived(responseData); + processDailyContent(responseData); + } + } + + public void processDailyContent(final String responseData) { + final SiteApiFeatureCollection response = GSON.fromJson(responseData, SiteApiFeatureCollection.class); + + if (response == null) { + return; + } + + final SiteApiFeatureProperties props = response.getFirstProperties(); + if (props == null) { + return; + } + + final String startOfHour = MetOfficeDataHubSiteHandler.getDataTsAtLastChronoUnit(ChronoUnit.DAYS); + final int forecastForthisHour = props.getHourlyTimeSeriesPositionForCurrentHour(startOfHour); + + for (int dayOffset = 0; dayOffset <= 6; ++dayOffset) { + // Calculate the correct array position for the data + final int dataIdx = (forecastForthisHour != -1) ? forecastForthisHour + dayOffset : -1; + + final String channelPrefix = MetOfficeDataHubSiteHandler.calculatePrefix(GROUP_PREFIX_DAILY_FORECAST, + dayOffset); + + final SiteApiTimeSeries data = props.getTimeSeries(dataIdx); + + updateState(channelPrefix + SITE_TIMESTAMP, getDateTimeTypeState(data.getTime())); + + updateState(channelPrefix + SITE_DAILY_MIDDAY_WIND_SPEED_10M, + getQuantityTypeState(data.getMidday10MWindSpeed(), Units.METRE_PER_SECOND)); + + updateState(channelPrefix + SITE_DAILY_MIDNIGHT_WIND_SPEED_10M, + getQuantityTypeState(data.getMidnight10MWindSpeed(), Units.METRE_PER_SECOND)); + + updateState(channelPrefix + SITE_DAILY_MIDDAY_WIND_DIRECTION_10M, + getQuantityTypeState(data.getMidday10MWindDirection(), Units.DEGREE_ANGLE)); + + updateState(channelPrefix + SITE_DAILY_MIDNIGHT_WIND_DIRECTION_10M, + getQuantityTypeState(data.getMidnight10MWindDirection(), Units.DEGREE_ANGLE)); + + updateState(channelPrefix + SITE_DAILY_MIDDAY_WIND_GUST_10M, + getQuantityTypeState(data.getMidday10MWindGust(), Units.METRE_PER_SECOND)); + + updateState(channelPrefix + SITE_DAILY_MIDNIGHT_WIND_GUST_10M, + getQuantityTypeState(data.getMidnight10MWindGust(), Units.METRE_PER_SECOND)); + + updateState(channelPrefix + SITE_DAILY_MIDDAY_VISIBILITY, + getQuantityTypeState(data.getMiddayVisibility(), METRE)); + + updateState(channelPrefix + SITE_DAILY_MIDNIGHT_VISIBILITY, + getQuantityTypeState(data.getMidnightVisibility(), METRE)); + + updateState(channelPrefix + SITE_DAILY_MIDDAY_REL_HUMIDITY, + getQuantityTypeState(data.getMiddayRelativeHumidity(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_MIDNIGHT_REL_HUMIDITY, + getQuantityTypeState(data.getMidnightRelativeHumidity(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_MIDDAY_PRESSURE, + getQuantityTypeState(data.getMiddayPressure(), SIUnits.PASCAL)); + + updateState(channelPrefix + SITE_DAILY_MIDNIGHT_PRESSURE, + getQuantityTypeState(data.getMidnightPressure(), SIUnits.PASCAL)); + + updateState(channelPrefix + SITE_DAILY_DAY_MAX_UV_INDEX, getDecimalTypeState(data.getMaxUvIndex())); + + updateState(channelPrefix + SITE_DAILY_DAY_UPPER_BOUND_MAX_TEMP, + getQuantityTypeState(data.getDayUpperBoundMaxTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_DAY_LOWER_BOUND_MAX_TEMP, + getQuantityTypeState(data.getDayLowerBoundMaxTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_UPPER_BOUND_MAX_TEMP, + getQuantityTypeState(data.getNightUpperBoundMinTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_LOWER_BOUND_MAX_TEMP, + getQuantityTypeState(data.getNightLowerBoundMinTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_FEELS_LIKE_MIN_TEMP, + getQuantityTypeState(data.getNightMinFeelsLikeTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_DAY_FEELS_LIKE_MAX_TEMP, + getQuantityTypeState(data.getDayMaxFeelsLikeTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_LOWER_BOUND_MIN_TEMP, + getQuantityTypeState(data.getNightLowerBoundMinTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_DAY_MAX_FEELS_LIKE_TEMP, + getQuantityTypeState(data.getDayMaxFeelsLikeTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_LOWER_BOUND_MIN_FEELS_LIKE_TEMP, + getQuantityTypeState(data.getNightLowerBoundMinFeelsLikeTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_DAY_LOWER_BOUND_MAX_FEELS_LIKE_TEMP, + getQuantityTypeState(data.getDayLowerBoundMaxFeelsLikeTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_DAY_UPPER_BOUND_MAX_FEELS_LIKE_TEMP, + getQuantityTypeState(data.getDayUpperBoundMaxFeelsLikeTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_UPPER_BOUND_MIN_FEELS_LIKE_TEMP, + getQuantityTypeState(data.getNightUpperBoundMinFeelsLikeTemp(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_DAY_MAX_SCREEN_TEMPERATURE, + getQuantityTypeState(data.getDayMaxScreenTemperature(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_MIN_SCREEN_TEMPERATURE, + getQuantityTypeState(data.getNightMinScreenTemperature(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_DAILY_DAY_PROBABILITY_OF_PRECIPITATION, + getQuantityTypeState(data.getDayProbabilityOfPrecipitation(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_PROBABILITY_OF_PRECIPITATION, + getQuantityTypeState(data.getNightProbabilityOfPrecipitation(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_DAY_PROBABILITY_OF_SNOW, + getQuantityTypeState(data.getDayProbabilityOfSnow(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_PROBABILITY_OF_SNOW, + getQuantityTypeState(data.getNightProbabilityOfSnow(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_DAY_PROBABILITY_OF_HEAVY_SNOW, + getQuantityTypeState(data.getDayProbabilityOfHeavySnow(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_PROBABILITY_OF_HEAVY_SNOW, + getQuantityTypeState(data.getNightProbabilityOfHeavySnow(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_DAY_PROBABILITY_OF_RAIN, + getQuantityTypeState(data.getDayProbabilityOfRain(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_PROBABILITY_OF_RAIN, + getQuantityTypeState(data.getNightProbabilityOfRain(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_DAY_PROBABILITY_OF_HEAVY_RAIN, + getQuantityTypeState(data.getDayProbabilityOfHeavyRain(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_PROBABILITY_OF_HEAVY_RAIN, + getQuantityTypeState(data.getNightProbabilityOfHeavyRain(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_DAY_PROBABILITY_OF_HAIL, + getQuantityTypeState(data.getDayProbabilityOfHail(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_PROBABILITY_OF_HAIL, + getQuantityTypeState(data.getNightProbabilityOfHail(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_DAY_PROBABILITY_OF_SFERICS, + getQuantityTypeState(data.getDayProbabilityOfSferics(), Units.PERCENT)); + + updateState(channelPrefix + SITE_DAILY_NIGHT_PROBABILITY_OF_SFERICS, + getQuantityTypeState(data.getNightProbabilityOfSferics(), Units.PERCENT)); + } + } + + @Override + public void processHourlyResponse(final String responseData, final String pollId) { + if (hourlyPollKey.equals(pollId)) { + hourlyForecastPollManager.setDataContentReceived(responseData); + processHourlyContent(responseData); + } + } + + public void processHourlyContent(final String responseData) { + final SiteApiFeatureCollection response = GSON.fromJson(responseData, SiteApiFeatureCollection.class); + + if (response == null) { + return; + } + + final SiteApiFeatureProperties props = response.getFirstProperties(); + if (props == null) { + return; + } + + final String startOfHour = MetOfficeDataHubSiteHandler.getDataTsAtLastChronoUnit(ChronoUnit.HOURS); + final int forecastForthisHour = props.getHourlyTimeSeriesPositionForCurrentHour(startOfHour); + + for (int hrOffset = 0; hrOffset <= 24; ++hrOffset) { + // Calculate the correct array position for the data + final int dataIdx = (forecastForthisHour != -1) ? forecastForthisHour + hrOffset : -1; + final SiteApiTimeSeries data = props.getTimeSeries(dataIdx); + + final String channelPrefix = MetOfficeDataHubSiteHandler.calculatePrefix(GROUP_PREFIX_HOURS_FORECAST, + hrOffset); + + updateState(channelPrefix + SITE_TIMESTAMP, getDateTimeTypeState(data.getTime())); + + updateState(channelPrefix + SITE_HOURLY_FORECAST_SCREEN_TEMPERATURE, + getQuantityTypeState(data.getScreenTemperature(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_HOURLY_FORECAST_MIN_SCREEN_TEMPERATURE, + getQuantityTypeState(data.getMinScreenTemperature(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_HOURLY_FORECAST_MAX_SCREEN_TEMPERATURE, + getQuantityTypeState(data.getMaxScreenTemperature(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_HOURLY_FEELS_LIKE_TEMPERATURE, + getQuantityTypeState(data.getFeelsLikeTemperature(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_HOURLY_SCREEN_RELATIVE_HUMIDITY, + getQuantityTypeState(data.getScreenRelativeHumidity(), Units.PERCENT)); + + updateState(channelPrefix + SITE_HOURLY_VISIBILITY, getQuantityTypeState(data.getVisibility(), METRE)); + + updateState(channelPrefix + SITE_HOURLY_PROBABILITY_OF_PRECIPITATION, + getQuantityTypeState(data.getProbOfPrecipitation(), Units.PERCENT)); + + updateState(channelPrefix + SITE_HOURLY_PRECIPITATION_RATE, + getQuantityTypeState(data.getPrecipitationRate(), Units.MILLIMETRE_PER_HOUR)); + + updateState(channelPrefix + SITE_HOURLY_TOTAL_PRECIPITATION_AMOUNT, + getQuantityTypeState(data.getTotalPrecipAmount(), MILLI(METRE))); + + updateState(channelPrefix + SITE_HOURLY_TOTAL_SNOW_AMOUNT, + getQuantityTypeState(data.getTotalSnowAmount(), MILLI(METRE))); + + updateState(channelPrefix + SITE_HOURLY_PRESSURE, getQuantityTypeState(data.getPressure(), SIUnits.PASCAL)); + + updateState(channelPrefix + SITE_HOURLY_WIND_SPEED_10M, + getQuantityTypeState(data.getWindSpeed10m(), Units.METRE_PER_SECOND)); + + updateState(channelPrefix + SITE_HOURLY_MAX_10M_WIND_GUST, + getQuantityTypeState(data.getMax10mWindGust(), Units.METRE_PER_SECOND)); + + updateState(channelPrefix + SITE_HOURLY_WIND_GUST_SPEED_10M, + getQuantityTypeState(data.getWindGustSpeed10m(), Units.METRE_PER_SECOND)); + + updateState(channelPrefix + SITE_HOURLY_SCREEN_DEW_POINT_TEMPERATURE, + getQuantityTypeState(data.getScreenDewPointTemperature(), SIUnits.CELSIUS)); + + updateState(channelPrefix + SITE_HOURLY_UV_INDEX, getDecimalTypeState(data.getUvIndex())); + + updateState(channelPrefix + SITE_HOURLY_WIND_DIRECTION_FROM_10M, + getQuantityTypeState(data.getWindDirectionFrom10m(), Units.DEGREE_ANGLE)); + } + } + + public static String getDataTsAtLastChronoUnit(final ChronoUnit unit) { + return Instant.now().truncatedTo(unit).toString().substring(0, 16) + "Z"; + } + + // Helpers for updating channels support + + private static String calculatePrefix(final String prefix, final int plusOffset) { + final StringBuilder strBldr = new StringBuilder(26); + strBldr.append(prefix); + if (plusOffset > 0) { + strBldr.append(GROUP_POSTFIX_BOTH_FORECASTS); + if (plusOffset < 10) { + strBldr.append("0"); + } + strBldr.append(plusOffset); + } + strBldr.append(GROUP_PREFIX_TO_ITEM); + return strBldr.toString(); + } + + protected State getDateTimeTypeState(@Nullable String value) { + return (value == null) ? UnDefType.UNDEF : new DateTimeType(value).toLocaleZone(); + } + + protected State getQuantityTypeState(@Nullable Number value, Unit unit) { + return (value == null) ? UnDefType.UNDEF : new QuantityType<>(value, unit); + } + + protected State getDecimalTypeState(@Nullable Number value) { + return (value == null) ? UnDefType.UNDEF : new DecimalType(value); + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDelayedExecutor.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDelayedExecutor.java new file mode 100644 index 0000000000000..bab5860bc2b95 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/MetOfficeDelayedExecutor.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal; + +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link MetOfficeDelayedExecutor} wraps up the executor functionality for a delayed execution with the + * relevant locking. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class MetOfficeDelayedExecutor { + + public MetOfficeDelayedExecutor(final ScheduledExecutorService scheduler) { + this.scheduler = scheduler; + } + + private final ScheduledExecutorService scheduler; + private final Object scheduledFutureRefLock = new Object(); + private @Nullable ScheduledFuture scheduledFutureRef = null; + + public void scheduleExecution(final long initialDelay, final Runnable task) { + synchronized (scheduledFutureRefLock) { + cancelScheduledTask(true); + scheduledFutureRef = scheduler.schedule(task, initialDelay, TimeUnit.MILLISECONDS); + } + } + + public void cancelScheduledTask(final boolean allowInterrupt) { + synchronized (scheduledFutureRefLock) { + ScheduledFuture job = scheduledFutureRef; + if (job != null) { + job.cancel(true); + scheduledFutureRef = null; + } + } + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/PollManager.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/PollManager.java new file mode 100644 index 0000000000000..00cbca3bafe44 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/PollManager.java @@ -0,0 +1,204 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal; + +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.DAY_IN_MILLIS; +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.EXPECTED_TS_FORMAT; +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.RANDOM_GENERATOR; + +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.metofficedatahub.internal.api.ResponseDataProcessor; +import org.openhab.core.i18n.TimeZoneProvider; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link PollManager} manages basic poll management functionality. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class PollManager { + + private volatile long lastForecastPoll = -1; + + private final Logger logger = LoggerFactory.getLogger(PollManager.class); + + private final String pollName; + + ResponseDataProcessor runCachedDataPoll; + Runnable runLiveDataPoll; + + Duration durationBetweenPolls; + + String lastRepsonse = ""; + + private @Nullable ScheduledFuture pollScheduled = null; + + private final Object pollScheduledLock = new Object(); + private final TimeZoneProvider timeZoneProvider; + + /** + * This handles the scheduling of an hourly forecast poll, to be applied with the given delay. + * When run, if requests the run-time of the next one is calculated and scheduled. + */ + private final MetOfficeDelayedExecutor forecastJob; + + private volatile boolean dataRequired; + + public PollManager(final String pollName, @Reference TimeZoneProvider timeZoneProvider, + @Reference ScheduledExecutorService scheduler, final Duration durationBetweenPolls, + final ResponseDataProcessor cachedDataPoll, final Runnable liveDataPoll) { + this.pollName = pollName; + this.durationBetweenPolls = durationBetweenPolls; + this.runCachedDataPoll = cachedDataPoll; + this.runLiveDataPoll = liveDataPoll; + this.timeZoneProvider = timeZoneProvider; + forecastJob = new MetOfficeDelayedExecutor(scheduler); + dataRequired = false; + } + + public void dispose() { + cancelScheduledPoll(true); + forecastJob.cancelScheduledTask(true); + } + + public void cachedPollOrLiveStart(final boolean attemptLivePollIfRequired) { + if (dataRequired) { + if (!lastRepsonse.isEmpty()) { + logger.trace("Using cached {} forecast response data", pollName); + runCachedDataPoll.processResponse(lastRepsonse); + } else { + logger.trace("Starting poll sequence for {} forecast data", pollName); + if (attemptLivePollIfRequired) { + reconfigurePolling(); + } + } + } else { + logger.trace("Skipping refresh on non-required data for {} forecast", pollName); + } + } + + public void setPollDuration(final Duration durationBetweenPolls) { + this.durationBetweenPolls = durationBetweenPolls; + } + + public void setDataRequired(final boolean dataRequired, final boolean reconfigureIfRequired) { + final boolean previousValue = this.dataRequired; + this.dataRequired = dataRequired; + + if (dataRequired) { + logger.trace("{} data poll required", pollName); + } else { + logger.trace("{} data poll not required", pollName); + } + + if (dataRequired) { + final boolean reconfigureRequired = reconfigureIfRequired && !previousValue; + if (reconfigureRequired) { + reconfigurePolling(); + } + cachedPollOrLiveStart(!reconfigureRequired); + } + } + + public boolean getIsDataRequired() { + return dataRequired; + } + + public void reconfigurePolling() { + final long millisSinceDayStart = getMillisSinceDayStart(); + final long pollRateMillis = durationBetweenPolls.toMillis(); + final long initialDelayTimeToFirstCycle = pollRateMillis - (millisSinceDayStart % pollRateMillis); + final long lastPollExpectedTime = System.currentTimeMillis() - (pollRateMillis - initialDelayTimeToFirstCycle); + + logger.trace("Last {} poll expected time should have been : {}", pollName, + millisToLocalDateTime(lastPollExpectedTime)); + + logger.trace("Last {} poll time should have been : {}", pollName, millisToLocalDateTime(lastForecastPoll)); + + // Poll if a poll hasn't been done before, or if the previous poll was before what would be now the new + // poll intervals last poll time then a poll should be run now. + if (lastForecastPoll == -1 || lastPollExpectedTime > lastForecastPoll) { + liveDataPoll(); + } else { + if (dataRequired && !lastRepsonse.isEmpty()) { + runCachedDataPoll.processResponse(lastRepsonse); + } + } + scheduleNextPoll(); + } + + private void cancelScheduledPoll(final boolean allowInterrupt) { + synchronized (pollScheduledLock) { + ScheduledFuture job = pollScheduled; + if (job != null) { + job.cancel(allowInterrupt); + pollScheduled = null; + } + } + } + + public void setDataContentReceived(final String responseContent) { + this.lastRepsonse = responseContent; + } + + protected static long getMillisSinceDayStart() { + return Duration.between(LocalDate.now().atStartOfDay(), LocalDateTime.now()).toMillis(); + } + + private String millisToLocalDateTime(final long milliseconds) { + ZonedDateTime cvDate = Instant.ofEpochMilli(milliseconds).atZone(timeZoneProvider.getTimeZone()); + return cvDate.format(DateTimeFormatter.ofPattern(EXPECTED_TS_FORMAT)); + } + + private void scheduleNextPoll() { + final long millisSinceDayStart = getMillisSinceDayStart(); + long pollRateMillis = durationBetweenPolls.toMillis(); + long initialDelayTimeToFirstCycle = pollRateMillis - (millisSinceDayStart % pollRateMillis); + if (initialDelayTimeToFirstCycle + millisSinceDayStart > DAY_IN_MILLIS) { + logger.debug("Not scheduling {} poll after next daily cycle reset", pollName); + } else { + logger.debug("Scheduling next {} forecast data poll to be in {} milliseconds at {}", pollName, + initialDelayTimeToFirstCycle, + millisToLocalDateTime(System.currentTimeMillis() + initialDelayTimeToFirstCycle)); + + initialDelayTimeToFirstCycle += RANDOM_GENERATOR.nextInt(60000); + // Schedule the first poll to occur after the given delay + forecastJob.scheduleExecution(initialDelayTimeToFirstCycle, () -> { + liveDataPoll(); + scheduleNextPoll(); + }); + } + } + + public void liveDataPoll() { + if (getIsDataRequired()) { + logger.debug("Doing a POLL for the {} forecast", pollName); + runLiveDataPoll.run(); + } else { + logger.debug("Skipping a POLL for the {} forecast", pollName); + } + }; +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/AuthTokenException.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/AuthTokenException.java new file mode 100644 index 0000000000000..4cb04a9b7a13e --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/AuthTokenException.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import java.io.Serial; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link AuthTokenException} should be thrown when the endpoint being communicated with + * does not appear to be a Tap Link Gateway device. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class AuthTokenException extends I18Exception { + @Serial + private static final long serialVersionUID = -7786449325604153947L; + + public AuthTokenException() { + super(); + } + + public AuthTokenException(final String message) { + super(message); + } + + public AuthTokenException(final Throwable cause) { + super(cause); + } + + public AuthTokenException(final String message, final Throwable cause) { + super(message, cause); + } + + public String getI18Key() { + return getI18Key("exception.bad-auth-token"); + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/I18Exception.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/I18Exception.java new file mode 100644 index 0000000000000..5cc2f84052427 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/I18Exception.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import java.io.Serial; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link I18Exception} is a abstract class for exceptions that support + * i18key functionality. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public abstract class I18Exception extends Exception { + + @Serial + private static final long serialVersionUID = -7784829349743963947L; + + protected String i18Key = ""; + + public I18Exception() { + super(); + } + + public I18Exception(final String message) { + super(message); + } + + public I18Exception(final Throwable cause) { + super(cause); + } + + public I18Exception(final String message, final Throwable cause) { + super(message, cause); + } + + public abstract String getI18Key(); + + public String getI18Key(final String defaultI18) { + if (!i18Key.isBlank()) { + return i18Key; + } + return Objects.requireNonNullElse(getMessage(), defaultI18); + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/IConnectionStatusListener.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/IConnectionStatusListener.java new file mode 100644 index 0000000000000..2851cf781a0a4 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/IConnectionStatusListener.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Implementations of this interface, allow access to a HttpClient which can be used + * for communication requests to LinkTap Gateways. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public interface IConnectionStatusListener { + + /** + * This is invoked to notify implementations of this interface, whether the connection has been + * successfully authenticated or given a response indicating an authentication failure, or that + * the authentication data is not in a valid format. + * + * @param authenticated is true when the authentication was validated and successful used. + */ + void processAuthenticationResult(final boolean authenticated); + + /** + * This is invoked to notify implementations of this interface, upon a failure of communications. + * + * @param t is instance of the Throwable that was raised/thrown when the communications failed to process. + */ + void processCommunicationFailure(final @Nullable Throwable t); + + /** + * This is invoked to notify implementations of this interface, when connectivity has been successful. + */ + void processConnected(); +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/IRateLimiterListener.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/IRateLimiterListener.java new file mode 100644 index 0000000000000..b1affeca5f317 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/IRateLimiterListener.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Implementations of this interface, allow the monitoring of when the rate limiter + * has updated its operating parameters. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public interface IRateLimiterListener { + + /** + * This is invoked to notify implementations of this interface, that the given rate limiter + * has been updated, with new counts. + * + * @param requestLimiter is a reference to the rate limiter that has been updated. + */ + void processRateLimiterUpdated(final RequestLimiter requestLimiter); +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/ISiteResponseListener.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/ISiteResponseListener.java new file mode 100644 index 0000000000000..e9ab70d14b875 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/ISiteResponseListener.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Implementations of this interface, allow the responses of a SiteAPI request to + * be processed + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public interface ISiteResponseListener { + + /** + * This is invoked to notify implementations of this interface, new daily response data has been received. + * It is at the implementations discretion whether the data is of interest based on the pollId, which is set + * from when the original poll was requested. + * + * @param content is the daily response JSON content returned for a site API request. + * @param pollId is the ID associated to the request this was requested with. + */ + void processDailyResponse(final String content, final String pollId); + + /** + * This is invoked to notify implementations of this interface, new hourly response data has been received. + * It is at the implementations discretion whether the data is of interest based on the pollId, which is set + * from when the original poll was requested. + * + * @param content is the hourly response JSON content returned for a site API request. + * @param pollId is the ID associated to the request this was requested with. + */ + void processHourlyResponse(final String content, final String pollId); +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/JwtTokenHeader.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/JwtTokenHeader.java new file mode 100644 index 0000000000000..85fc53904c29a --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/JwtTokenHeader.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link JwtTokenHeader} allows the basic decoding of a JWT token header, to allow + * basic validation before using it. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class JwtTokenHeader { + @SerializedName("x5t") + private String x5t = ""; + + @SerializedName("kid") + private String kid = ""; + + @SerializedName("typ") + private String typ = ""; + + @SerializedName("alg") + private String alg = ""; + + public boolean isValid() { + if (x5t.isBlank() || kid.isBlank() || typ.isBlank() || alg.isBlank()) { + return false; + } + + return "JWT".contentEquals(typ); + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/JwtTokenPayload.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/JwtTokenPayload.java new file mode 100644 index 0000000000000..f7f5a4dd70a3a --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/JwtTokenPayload.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link JwtTokenPayload} allows the basic decoding of a JWT token header, to allow + * basic validation before using it. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class JwtTokenPayload { + @SerializedName("sub") + private String sub = ""; + + @SerializedName("iss") + private String iss = ""; + + @SerializedName("keytype") + private String keyType = ""; + + @SerializedName("token_type") + private String tokenType = ""; + + public boolean isValid() { + if (sub.isBlank() || iss.isBlank() || keyType.isBlank() || tokenType.isBlank()) { + return false; + } + + return "apiKey".contentEquals(tokenType); + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/LookupWrapper.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/LookupWrapper.java new file mode 100644 index 0000000000000..b15d895032484 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/LookupWrapper.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.validation.constraints.NotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link LookupWrapper} is a container providing common functionality for providing + * key -> T mappings. The backend store is ConcurrentHashMap. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class LookupWrapper<@Nullable itemT> { + + final Map<@NotNull String, @Nullable itemT> storeLookup = new ConcurrentHashMap<>(); + + /** + * Register using key the given T instance, and after addition call the specified Runnable + * + * @param key - The key for the item + * @param item - The instance to store a reference to + * @param afterAddition - The runnable to run after the addition has been completed + * @return - false if another item is already assigned to the key preventing the addition, or true + * when added successfully. + */ + public boolean registerItem(final @NotNull String key, final @NotNull itemT item, + @NotNull final Runnable afterAddition) { + if (storeLookup.containsKey(key)) { + final itemT found = storeLookup.get(key); + if (found != null && !found.equals(item)) { + return false; + } + } + storeLookup.put(key, item); + afterAddition.run(); + return true; + } + + /** + * Remove the given key and item combination + * + * @param key - The expected key of the item + * @param item - The item referenced by the key + * @param whenEmpty - Runnable executed when no more key -> item mappings exist + */ + public void deregisterItem(final @NotNull String key, final @NotNull itemT item, + @NotNull final Runnable whenEmpty) { + storeLookup.remove(key, item); + if (storeLookup.isEmpty()) { + whenEmpty.run(); + } + } + + /** + * Returns the item associated to the given key + * + * @param key - the key to find the item for + * @return - null if no item is found otherwise the found item + */ + public @Nullable itemT getItem(final @NotNull String key) { + return storeLookup.get(key); + } + + /** + * Clears a entry when only the given key is known + * + * @param key - the key remove if it exists + */ + public void clearItem(final @NotNull String key) { + storeLookup.remove(key); + } + + /** + * Get an array of all entries stored at the time of calling + */ + public List<@NotNull itemT> getItemlist() { + return storeLookup.values().stream().toList(); + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/RequestLimiter.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/RequestLimiter.java new file mode 100644 index 0000000000000..be0ae4d9df73c --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/RequestLimiter.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoUnit; +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.storage.Storage; +import org.openhab.core.storage.StorageService; +import org.osgi.framework.Bundle; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link RequestLimiter} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class RequestLimiter { + + public static final int INVALID_REQUEST_ID = -1; + public static final int SECONDS_PER_DAY = 86400; + + static final ZoneId UTC_ZONE_ID = ZoneId.of("UTC"); + + final StorageService storageService; + final ScheduledExecutorService scheduler; + final TimeZoneProvider timeZoneProvider; + final TranslationProvider translationProvider; + final LocaleProvider localeProvider; + final Bundle bundle; + + private final Logger logger = LoggerFactory.getLogger(RequestLimiter.class); + private final Object dailyResetLock = new Object(); + + private int requestLimit = 0; + private int currentRequestCount = 0; + private String limiterId; + private String storageKeyCount; + private String storageKeyTimestamp; + private @Nullable ScheduledFuture dailyResetFuture = null; + + public int getCurrentRequestCount() { + return currentRequestCount; + } + + public RequestLimiter(final String limiterId, @Reference StorageService storageService, + @Reference TimeZoneProvider timeZoneProvider, ScheduledExecutorService scheduler, + @Reference TranslationProvider translationProvider, @Reference LocaleProvider localeProvider, + @Reference Bundle bundle) { + this.limiterId = limiterId; + this.storageKeyCount = limiterId + "_count"; + this.storageKeyTimestamp = limiterId + "_ts"; + this.storageService = storageService; + this.timeZoneProvider = timeZoneProvider; + this.scheduler = scheduler; + this.translationProvider = translationProvider; + this.localeProvider = localeProvider; + this.bundle = bundle; + loadLimiterData(); + } + + public void dispose() { + cancelPendingDailyReset(); + saveLimiterData(); + } + + private void scheduleStandardResetTime(final int resetLimit) { + // Met Office Data Hub resets at midnight UTC tz it's daily counter call's + final ZonedDateTime currentTs = Instant.now().atZone(UTC_ZONE_ID); + final ZonedDateTime resetTs = currentTs.minusHours(currentTs.getHour()).minusMinutes(currentTs.getMinute()) + .minusSeconds(currentTs.getSecond()).minusNanos(currentTs.getNano()).plusDays(1); + scheduleDailyReset(resetTs, resetLimit); + } + + private void cancelPendingDailyReset() { + synchronized (dailyResetLock) { + final ScheduledFuture ref = dailyResetFuture; + if (ref != null) { + ref.cancel(true); + dailyResetFuture = null; + } + } + } + + private void scheduleDailyReset(final ZonedDateTime resetLimiterDailyTime, final int resetLimit) { + final ZonedDateTime currentTs = Instant.now().atZone(resetLimiterDailyTime.getZone()); + final long secondsUnitReset = ChronoUnit.SECONDS.between(currentTs, resetLimiterDailyTime); + synchronized (dailyResetLock) { + cancelPendingDailyReset(); + dailyResetFuture = scheduler.scheduleWithFixedDelay(() -> { + resetLimiter(resetLimit); + }, secondsUnitReset, SECONDS_PER_DAY, TimeUnit.SECONDS); + } + } + + private void loadLimiterData() { + final Storage storage = storageService.getStorage(limiterId, String.class.getClassLoader()); + @Nullable + final String countStored = storage.get(storageKeyCount); + @Nullable + final String tsStored = storage.get(storageKeyTimestamp); + if (countStored != null && tsStored != null) { + int newCount = -1; + + try { + newCount = Integer.parseInt(countStored); + ZonedDateTime newTs = ZonedDateTime.parse(tsStored, DateTimeFormatter.ISO_ZONED_DATE_TIME); + final ZonedDateTime currentTs = Instant.now().atZone(UTC_ZONE_ID); + if (newTs.getDayOfYear() == currentTs.getDayOfYear()) { + currentRequestCount = newCount; + logger.trace("Limiter {} -> Restored Request Limiter count of {} for day of year {}", limiterId, + currentRequestCount, currentTs.getDayOfYear()); + } else { + logger.trace("Limiter {} -> Days of saved data are not the same not restoring {} != {}", limiterId, + newTs.getDayOfYear(), currentTs.getDayOfYear()); + } + } catch (DateTimeParseException | NumberFormatException exception) { + logger.warn("{}", + getLocalizedText("api.log.rate-limiter.failed-restore", limiterId, exception.getMessage()), + exception); + } + } + } + + private void saveLimiterData() { + final ZonedDateTime saveTime = Instant.now().atZone(UTC_ZONE_ID); + final Storage storage = storageService.getStorage(limiterId, String.class.getClassLoader()); + storage.put(storageKeyCount, String.valueOf(currentRequestCount)); + storage.put(storageKeyTimestamp, saveTime.format(DateTimeFormatter.ISO_INSTANT)); + logger.trace("Limiter {} -> Persisted Request Limiter count of {} for date {}", limiterId, currentRequestCount, + saveTime.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)); + } + + public void resetLimiter() { + resetLimiter(requestLimit); + } + + public synchronized void resetLimiter(int newLimit) { + requestLimit = newLimit; + currentRequestCount = 0; + logger.trace("Limiter {} -> Resetting limiter to 0 used for new limit {}", limiterId, newLimit); + scheduler.schedule(this::saveLimiterData, 1, TimeUnit.SECONDS); + } + + public synchronized void updateLimit(int newLimit) { + requestLimit = newLimit; + logger.trace("Limiter {} -> Updated limiter to new total limit {}", limiterId, newLimit); + scheduler.schedule(this::saveLimiterData, 1, TimeUnit.SECONDS); + scheduleStandardResetTime(newLimit); + } + + public synchronized int getRequestCountIfAvailable() { + final int requestId = currentRequestCount; + if (currentRequestCount < requestLimit) { + ++currentRequestCount; + scheduler.schedule(this::saveLimiterData, 1, TimeUnit.SECONDS); + } else { + return INVALID_REQUEST_ID; + } + return requestId; + } + + public boolean isInvalidRequestId(final int requestId) { + return INVALID_REQUEST_ID == requestId; + } + + // Localization functionality + + public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) { + String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments); + return Objects.nonNull(result) ? result : key; + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/ResponseDataProcessor.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/ResponseDataProcessor.java new file mode 100644 index 0000000000000..9fa3a3aa1c002 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/ResponseDataProcessor.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Implementations of this interface, allow the responses of a SiteAPI request to + * be processed + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public abstract class ResponseDataProcessor { + public abstract void processResponse(final String content); +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/SiteApi.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/SiteApi.java new file mode 100644 index 0000000000000..3b3f7c873e45c --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/SiteApi.java @@ -0,0 +1,267 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.*; + +import java.util.Objects; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.client.util.BufferingResponseListener; +import org.eclipse.jetty.http.HttpMethod; +import org.openhab.core.i18n.LocaleProvider; +import org.openhab.core.i18n.TimeZoneProvider; +import org.openhab.core.i18n.TranslationProvider; +import org.openhab.core.io.net.http.HttpClientFactory; +import org.openhab.core.library.types.PointType; +import org.openhab.core.storage.StorageService; +import org.osgi.framework.Bundle; +import org.osgi.framework.FrameworkUtil; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This provides the communications layer for the Site API + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class SiteApi { + + protected final LookupWrapper<@Nullable IConnectionStatusListener> authenticationListeners = new LookupWrapper<>(); + protected final LookupWrapper<@Nullable IRateLimiterListener> rateLimiterListeners = new LookupWrapper<>(); + + private final Logger logger = LoggerFactory.getLogger(SiteApi.class); + + // Utilised for sending communications + private final HttpClient httpClient; + + // Utilised for persistence of rate limiter data between reboots + private final ScheduledExecutorService scheduler; + + private final SiteApiAuthentication apiAuth; + private final RequestLimiter requestLimiter; + + private final String usageId; + + private final TranslationProvider translationProvider; + private final LocaleProvider localeProvider; + private final Bundle bundle; + + @Activate + public SiteApi(String usageId, @Reference HttpClientFactory httpClientFactory, + @Reference StorageService storageService, @Reference TranslationProvider translationProvider, + @Reference LocaleProvider localeProvider, @Reference TimeZoneProvider timeZoneProvider, + @Reference ScheduledExecutorService scheduler) { + this.usageId = usageId; + this.httpClient = httpClientFactory.getCommonHttpClient(); + this.apiAuth = new SiteApiAuthentication(); + this.translationProvider = translationProvider; + this.localeProvider = localeProvider; + this.bundle = FrameworkUtil.getBundle(getClass()); + this.requestLimiter = new RequestLimiter(usageId, storageService, timeZoneProvider, scheduler, + translationProvider, localeProvider, bundle); + this.scheduler = scheduler; + } + + public void registerListeners(final String id, final Object candidateListener) { + if (candidateListener instanceof IConnectionStatusListener connectionStatusListener) { + authenticationListeners.registerItem(id, connectionStatusListener, NO_OP); + } + if (candidateListener instanceof IRateLimiterListener rateLimiterListener) { + rateLimiterListeners.registerItem(id, rateLimiterListener, NO_OP); + } + } + + public void deregisterListeners(final String id, final Object candidateListener) { + if (candidateListener instanceof IConnectionStatusListener connectionStatusListener) { + authenticationListeners.deregisterItem(id, connectionStatusListener, NO_OP); + } + if (candidateListener instanceof IRateLimiterListener rateLimiterListener) { + rateLimiterListeners.deregisterItem(id, rateLimiterListener, NO_OP); + } + } + + public void dispose() { + requestLimiter.dispose(); + apiAuth.dispose(); + } + + public void setLimits(final int maxDailyCallLimit) { + requestLimiter.updateLimit(maxDailyCallLimit); + } + + public void setApiKey(final String apiKey) { + try { + apiAuth.setApiKey(apiKey); + } catch (AuthTokenException ate) { + notifyAuthenticationListeners(false); + } + } + + private void notifyRateLimiterListeners() { + scheduler.execute(() -> { + rateLimiterListeners.getItemlist().forEach(rateLimiterListener -> { + if (rateLimiterListener != null) { + rateLimiterListener.processRateLimiterUpdated(requestLimiter); + } + }); + }); + } + + private void notifyAuthenticationListeners(final boolean authenticated) { + scheduler.execute(() -> { + authenticationListeners.getItemlist().forEach(authListener -> { + if (authListener != null) { + authListener.processAuthenticationResult(authenticated); + } + }); + }); + } + + private void notifyCommFailureListeners(final @Nullable Throwable e) { + scheduler.execute(() -> { + authenticationListeners.getItemlist().forEach(authListener -> { + if (authListener != null) { + authListener.processCommunicationFailure(e); + } + }); + }); + } + + private void notifyConnectedListeners() { + scheduler.execute(() -> { + authenticationListeners.getItemlist().forEach(authListener -> { + if (authListener != null) { + authListener.processConnected(); + } + }); + }); + } + + public void validateSiteApi() { + final PointType siteApiTestLocation = new PointType("51.5072,0.1276"); + final Response.CompleteListener siteResponseListener = new BufferingResponseListener() { // 4.5kb buffer + @Override + public void onComplete(@Nullable Result result) { + if (result != null && !result.isFailed()) { + notifyConnectedListeners(); + } else { + if (result != null) { + notifyCommFailureListeners(result.getFailure()); + } else { + notifyCommFailureListeners(new Throwable("Unknown")); + } + } + } + }; + + if (requestLimiter.getRequestCountIfAvailable() == RequestLimiter.INVALID_REQUEST_ID) { + logger.warn("{}", getLocalizedText("comm.comm-check.no-quota-left")); + notifyConnectedListeners(); + return; + } + + sendAsyncSiteApiRequest(true, siteApiTestLocation, siteResponseListener, "validateSiteApiCheck"); + } + + public boolean sendRequest(final boolean daily, final PointType location, + final ISiteResponseListener siteResponseListener, final String pollId) { + if (requestLimiter.getRequestCountIfAvailable() == RequestLimiter.INVALID_REQUEST_ID) { + logger.debug("{} - Disabled requesting data - request limit has been hit", usageId); + return false; + } + + notifyRateLimiterListeners(); + + final Response.CompleteListener listener = new BufferingResponseListener() { // 4.5kb buffer will cover both + @Override + public void onComplete(@Nullable Result result) { + if (result != null) { + final boolean userAuthValidatedPreviously = apiAuth.getIsAuthenticated(); + apiAuth.processResult(result); + if (!apiAuth.getIsAuthenticated()) { + // Callback Async function confirming authorization is completed. + notifyAuthenticationListeners(false); + return; + } + + // User is authorized at this point + if (!userAuthValidatedPreviously) { + // Authorization token confirmed + // Callback Async function confirming authorization is completed. + notifyAuthenticationListeners(true); + } + + if (result.isSucceeded()) { + final String response = getContentAsString(); + if (response != null) { + logger.trace("Got response for poll ID: \"{}\"", pollId); + notifyConnectedListeners(); + scheduler.execute(() -> { + if (daily) { + siteResponseListener.processDailyResponse(response, pollId); + } else { + siteResponseListener.processHourlyResponse(response, pollId); + } + }); + } + } else { + notifyCommFailureListeners(result.getFailure()); + } + } + } + }; + + sendAsyncSiteApiRequest(daily, location, listener, pollId); + return true; + } + + public void sendAsyncSiteApiRequest(final boolean daily, final PointType location, + final Response.CompleteListener listener, final String pollId) { + final String url = ((daily) ? GET_FORECAST_URL_DAILY : GET_FORECAST_URL_HOURLY) + .replace(GET_FORECAST_KEY_LATITUDE, location.getLatitude().toString()) + .replace(GET_FORECAST_KEY_LONGITUDE, location.getLongitude().toString()); + + final Request request = httpClient.newRequest(url).method(HttpMethod.GET) + .header(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_TYPE.toString()) + .timeout(GET_FORECAST_REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + + logger.trace("Requesting using Poll ID \"{}\" URL: \"{}\"", pollId, url); + + try { + apiAuth.addAuthentication(request).send(listener); + } catch (AuthTokenException ate) { + notifyAuthenticationListeners(false); + } + } + + // Localization functionality + + public String getLocalizedText(String key, @Nullable Object @Nullable... arguments) { + String result = translationProvider.getText(bundle, key, key, localeProvider.getLocale(), arguments); + return Objects.nonNull(result) ? result : key; + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/SiteApiAuthentication.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/SiteApiAuthentication.java new file mode 100644 index 0000000000000..de3723817a074 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/api/SiteApiAuthentication.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.GET_FORECAST_API_KEY_HEADER; +import static org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants.GSON; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Result; +import org.eclipse.jetty.http.HttpStatus; + +import com.google.gson.JsonSyntaxException; + +/** + * This handles the authentication aspects of the Site API + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class SiteApiAuthentication { + + private final Object isAuthenticatedWriteLock = new Object(); + + private Boolean isAuthenticated = false; + private String apiKey = ""; + + public SiteApiAuthentication() { + } + + public void dispose() { + } + + public void setApiKey(final String newApiKey) throws AuthTokenException { + this.apiKey = ""; + + // Perform some basic token checks, as data that isn't even JWT formatted will give a + // 500 response from the Met Office servers rather than a 401 response. + final String[] chunks = newApiKey.split("\\."); + if (chunks.length != 3) { + throw new AuthTokenException(); + } + final Base64.Decoder decoder = Base64.getUrlDecoder(); + try { + final JwtTokenHeader headers = GSON.fromJson(new String(decoder.decode(chunks[0]), StandardCharsets.UTF_8), + JwtTokenHeader.class); + if (headers == null || !headers.isValid()) { + throw new AuthTokenException(); + } + + final JwtTokenPayload payload = GSON.fromJson(new String(decoder.decode(chunks[1]), StandardCharsets.UTF_8), + JwtTokenPayload.class); + if (payload == null || !payload.isValid()) { + throw new AuthTokenException(); + } + + decoder.decode(chunks[2]); // check base64 encoding of signature + } catch (JsonSyntaxException | IllegalArgumentException e) { + throw new AuthTokenException(); + } + this.apiKey = newApiKey; + } + + /** + * Adds the required data to the Jetty Request, for the authentication of the request + * + * @param req is the request to have the relevant headers, etc added + * @return Request is the passed to Request arg to allow chained call's. + */ + public Request addAuthentication(final Request req) throws AuthTokenException { + if (apiKey.isBlank()) { + throw new AuthTokenException(); + } + return req.header(GET_FORECAST_API_KEY_HEADER, apiKey); + } + + /** + * Process the given Result from a Jetty Client request, and returns true if the Result + * indicates that there was not a authentication issue. + * + * @param result is the Jetty Client Result to check for authentication issues + * @return Result is the passed to Result arg to allow chained call's. + */ + public Result processResult(final Result result) { + if (result.isSucceeded()) { + switch (result.getResponse().getStatus()) { + case HttpStatus.FORBIDDEN_403: + setIsAuthenticated(false); + break; + case HttpStatus.OK_200: + setIsAuthenticated(true); + break; + } + } + return result; + } + + /** + * Return whether the current key has been validated as authenticated + * + * @return true if the last result processed indicated a validated api key. + */ + public boolean getIsAuthenticated() { + return this.isAuthenticated; + } + + /** + * Set's the state of the isAuthenticated state, using COW principles. + */ + private void setIsAuthenticated(final boolean isAuthenticated) { + synchronized (isAuthenticatedWriteLock) { + this.isAuthenticated = Boolean.valueOf(isAuthenticated); + } + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeature.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeature.java new file mode 100644 index 0000000000000..505a5167cd93c --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeature.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SiteApiFeature} is a Java class used as a DTO to hold part of the response to the Site Specific API. + * + * @author David Goodyear - Initial contribution + */ +public class SiteApiFeature extends SiteApiTypedResponseObject { + + public static final String TYPE_SITE_API_FEATURE = "Feature"; + + @Override + public String getExpectedType() { + return TYPE_SITE_API_FEATURE; + } + + @SerializedName("geometry") + private SiteApiFeaturePoint geometry; + + public SiteApiFeaturePoint getGeometry() { + return geometry; + } + + @SerializedName("properties") + private SiteApiFeatureProperties properties; + + public SiteApiFeatureProperties getProperties() { + return properties; + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureCollection.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureCollection.java new file mode 100644 index 0000000000000..551610a32ab7b --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureCollection.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SiteApiFeatureCollection} is a Java class used as a DTO to hold the response to the Site Specific API. + * + * @author David Goodyear - Initial contribution + */ +public class SiteApiFeatureCollection extends SiteApiTypedResponseObject { + + public static final String TYPE_SITE_API_FEATURE_COLLECTION = "FeatureCollection"; + + @Override + public String getExpectedType() { + return TYPE_SITE_API_FEATURE_COLLECTION; + } + + @SerializedName("features") + private SiteApiFeature[] feature; + + public SiteApiFeature[] getFeature() { + return feature; + } + + public SiteApiFeatureProperties getFirstProperties() { + if (feature == null || feature.length == 0) { + return null; + } + + return feature[0].getProperties(); + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeaturePoint.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeaturePoint.java new file mode 100644 index 0000000000000..83d1b944f90dc --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeaturePoint.java @@ -0,0 +1,45 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SiteApiFeaturePoint} is a Java class used as a DTO to hold part of the response to the Site Specific API. + * + * @author David Goodyear - Initial contribution + */ +public class SiteApiFeaturePoint extends SiteApiTypedResponseObject { + + public static final String TYPE_SITE_API_FEATURE = "Point"; + + @Override + public String getExpectedType() { + return TYPE_SITE_API_FEATURE; + } + + @SerializedName("coordinates") + private double[] coordinates; + + public double getLongitude() { + return coordinates[0]; + } + + public double getLatitude() { + return coordinates[1]; + } + + public double getElevation() { + return coordinates[2]; + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureProperties.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureProperties.java new file mode 100644 index 0000000000000..9ef4bd44f66cb --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureProperties.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.dto.responses; + +import java.util.HashMap; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SiteApiFeatureProperties} is a Java class used as a DTO to hold part of the response to the Site Specific + * API. + * + * @author David Goodyear - Initial contribution + */ +public class SiteApiFeatureProperties { + + public class SiteLocation { + @SerializedName("name") + private String name; + + public String getName() { + return name; + } + } + + @SerializedName("location") + private SiteLocation location; + + public SiteLocation getLocation() { + return location; + } + + @SerializedName("requestPointDistance") + private double requestPointDistance; + + public double getRequestPointDistance() { + return requestPointDistance; + } + + @SerializedName("modelRunDate") + private String modelRunDate; + + public String getModelRunDate() { + return modelRunDate; + } + + @SerializedName("timeSeries") + private SiteApiTimeSeries[] timeSeries; + + public SiteApiTimeSeries[] getTimeSeries() { + return timeSeries; + } + + public SiteApiTimeSeries getTimeSeries(final int position) { + if (position < 0 || position > timeSeries.length - 1) { + return EMPTY_TIME_SERIES; + } else { + return timeSeries[position]; + } + } + + public SiteApiTimeSeries getTimeSeries(final String timestamp) { + return getTimeSeries(getHourlyTimeSeriesPositionForCurrentHour(timestamp)); + } + + public static final SiteApiTimeSeries EMPTY_TIME_SERIES = new SiteApiTimeSeries(); + + private HashMap timeseriesPositions = null; + + public int getHourlyTimeSeriesPositionForCurrentHour(String timestamp) { + if (timeseriesPositions == null) { + timeseriesPositions = new HashMap<>(); + // Populate the lookup table + for (int i = 0; i < timeSeries.length; i++) { + timeseriesPositions.put(timeSeries[i].getTime(), i); + } + } + Integer result = timeseriesPositions.get(timestamp); + if (result == null) { + return -1; + } else { + return result.intValue(); + } + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiTimeSeries.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiTimeSeries.java new file mode 100644 index 0000000000000..553012f94e0ec --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiTimeSeries.java @@ -0,0 +1,458 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SiteApiTimeSeries} is a Java class used as a DTO to hold part of the response to the Site Specific + * API. + * + * @author David Goodyear - Initial contribution + */ +public class SiteApiTimeSeries { + + @SerializedName("time") + private String time; + + public String getTime() { + return time; + } + + /** + * Fields below relate to the hourly data-model + */ + @SerializedName("screenTemperature") + private Double screenTemperature; + + public Double getScreenTemperature() { + return screenTemperature; + } + + @SerializedName("maxScreenAirTemp") + private Double maxScreenAirTemp; + + public Double getMaxScreenTemperature() { + return maxScreenAirTemp; + } + + @SerializedName("minScreenAirTemp") + private Double minScreenAirTemp; + + public Double getMinScreenTemperature() { + return minScreenAirTemp; + } + + @SerializedName("screenDewPointTemperature") + private Double screenDewPointTemperature; + + public Double getScreenDewPointTemperature() { + return screenDewPointTemperature; + } + + @SerializedName("feelsLikeTemperature") + private Double feelsLikeTemperature; + + public Double getFeelsLikeTemperature() { + return feelsLikeTemperature; + } + + @SerializedName("windSpeed10m") + private Double windSpeed10m; + + public Double getWindSpeed10m() { + return windSpeed10m; + } + + @SerializedName("windDirectionFrom10m") + private Double windDirectionFrom10m; + + public Double getWindDirectionFrom10m() { + return windDirectionFrom10m; + } + + @SerializedName("max10mWindGust") + private Double max10mWindGust; + + public Double getMax10mWindGust() { + return max10mWindGust; + } + + @SerializedName("windGustSpeed10m") + private Double windGustSpeed10m; + + public Double getWindGustSpeed10m() { + return windGustSpeed10m; + } + + @SerializedName("visibility") + private Integer visibility; + + public Integer getVisibility() { + return visibility; + } + + @SerializedName("screenRelativeHumidity") + private Double screenRelativeHumidity; + + public Double getScreenRelativeHumidity() { + return screenRelativeHumidity; + } + + @SerializedName("mslp") + private Integer pressure; + + public Integer getPressure() { + return pressure; + } + + @SerializedName("uvIndex") + private Integer uvIndex; + + public Integer getUvIndex() { + return uvIndex; + } + + @SerializedName("significantWeatherCode") + private Integer significantWeatherCode; + + public Integer getSignificantWeatherCode() { + return significantWeatherCode; + } + + @SerializedName("precipitationRate") + private Double precipitationRate; + + public Double getPrecipitationRate() { + return precipitationRate; + } + + @SerializedName("totalPrecipAmount") + private Double totalPrecipAmount; + + public Double getTotalPrecipAmount() { + return totalPrecipAmount; + } + + @SerializedName("totalSnowAmount") + private Double totalSnowAmount; + + public Double getTotalSnowAmount() { + return totalSnowAmount; + } + + @SerializedName("probOfPrecipitation") + private Double probOfPrecipitation; + + public Double getProbOfPrecipitation() { + return probOfPrecipitation; + } + + /** + * Fields below relate to the daily data-model + */ + + @SerializedName("midday10MWindSpeed") + private Double midday10MWindSpeed; + + public Double getMidday10MWindSpeed() { + return midday10MWindSpeed; + } + + @SerializedName("midnight10MWindSpeed") + private Double midnight10MWindSpeed; + + public Double getMidnight10MWindSpeed() { + return midnight10MWindSpeed; + } + + @SerializedName("midday10MWindDirection") + private Integer midday10MWindDirection; + + public Integer getMidday10MWindDirection() { + return midday10MWindDirection; + } + + @SerializedName("midnight10MWindDirection") + private Integer midnight10MWindDirection; + + public Integer getMidnight10MWindDirection() { + return midnight10MWindDirection; + } + + @SerializedName("midday10MWindGust") + private Double midday10MWindGust; + + public Double getMidday10MWindGust() { + return midday10MWindGust; + } + + @SerializedName("midnight10MWindGust") + private Double midnight10MWindGust; + + public Double getMidnight10MWindGust() { + return midnight10MWindGust; + } + + @SerializedName("middayVisibility") + private Integer middayVisibility; + + public Integer getMiddayVisibility() { + return middayVisibility; + } + + @SerializedName("midnightVisibility") + private Integer midnightVisibility; + + public Integer getMidnightVisibility() { + return midnightVisibility; + } + + @SerializedName("middayRelativeHumidity") + private Double middayRelativeHumidity; + + public Double getMiddayRelativeHumidity() { + return middayRelativeHumidity; + } + + @SerializedName("midnightRelativeHumidity") + private Double midnightRelativeHumidity; + + public Double getMidnightRelativeHumidity() { + return midnightRelativeHumidity; + } + + @SerializedName("middayMslp") + private Integer middayPressure; + + public Integer getMiddayPressure() { + return middayPressure; + } + + @SerializedName("midnightMslp") + private Integer midnightPressure; + + public Integer getMidnightPressure() { + return midnightPressure; + } + + @SerializedName("maxUvIndex") + private Integer maxUvIndex; + + public Integer getMaxUvIndex() { + return maxUvIndex; + } + + @SerializedName("daySignificantWeatherCode") + private Integer daySignificantWeatherCode; + + public Integer getDaySignificantWeatherCode() { + return daySignificantWeatherCode; + } + + @SerializedName("nightSignificantWeatherCode") + private Integer nightSignificantWeatherCode; + + public Integer getNightSignificantWeatherCode() { + return nightSignificantWeatherCode; + } + + @SerializedName("dayMaxScreenTemperature") + private Double dayMaxScreenTemperature; + + public Double getDayMaxScreenTemperature() { + return dayMaxScreenTemperature; + } + + @SerializedName("nightMinScreenTemperature") + private Double nightMinScreenTemperature; + + public Double getNightMinScreenTemperature() { + return nightMinScreenTemperature; + } + + @SerializedName("dayUpperBoundMaxTemp") + private Double dayUpperBoundMaxTemp; + + public Double getDayUpperBoundMaxTemp() { + return dayUpperBoundMaxTemp; + } + + @SerializedName("dayUpperBoundMinTemp") + private Double dayUpperBoundMinTemp; + + public Double getDayUpperBoundMinTemp() { + return dayUpperBoundMinTemp; + } + + @SerializedName("nightUpperBoundMinTemp") + private Double nightUpperBoundMinTemp; + + public Double getNightUpperBoundMinTemp() { + return nightUpperBoundMinTemp; + } + + @SerializedName("dayLowerBoundMaxTemp") + private Double dayLowerBoundMaxTemp; + + public Double getDayLowerBoundMaxTemp() { + return dayLowerBoundMaxTemp; + } + + @SerializedName("nightLowerBoundMinTemp") + private Double nightLowerBoundMinTemp; + + public Double getNightLowerBoundMinTemp() { + return nightLowerBoundMinTemp; + } + + @SerializedName("dayMaxFeelsLikeTemp") + private Double dayMaxFeelsLikeTemp; + + public Double getDayMaxFeelsLikeTemp() { + return dayMaxFeelsLikeTemp; + } + + @SerializedName("nightMinFeelsLikeTemp") + private Double nightMinFeelsLikeTemp; + + public Double getNightMinFeelsLikeTemp() { + return nightMinFeelsLikeTemp; + } + + @SerializedName("dayUpperBoundMaxFeelsLikeTemp") + private Double dayUpperBoundMaxFeelsLikeTemp; + + public Double getDayUpperBoundMaxFeelsLikeTemp() { + return dayUpperBoundMaxFeelsLikeTemp; + } + + @SerializedName("nightUpperBoundMinFeelsLikeTemp") + private Double nightUpperBoundMinFeelsLikeTemp; + + public Double getNightUpperBoundMinFeelsLikeTemp() { + return nightUpperBoundMinFeelsLikeTemp; + } + + @SerializedName("dayLowerBoundMaxFeelsLikeTemp") + private Double dayLowerBoundMaxFeelsLikeTemp; + + public Double getDayLowerBoundMaxFeelsLikeTemp() { + return dayLowerBoundMaxFeelsLikeTemp; + } + + @SerializedName("nightLowerBoundMinFeelsLikeTemp") + private Double nightLowerBoundMinFeelsLikeTemp; + + public Double getNightLowerBoundMinFeelsLikeTemp() { + return nightLowerBoundMinFeelsLikeTemp; + } + + @SerializedName("dayProbabilityOfPrecipitation") + private Double dayProbabilityOfPrecipitation; + + public Double getDayProbabilityOfPrecipitation() { + return dayProbabilityOfPrecipitation; + } + + @SerializedName("nightProbabilityOfPrecipitation") + private Double nightProbabilityOfPrecipitation; + + public Double getNightProbabilityOfPrecipitation() { + return nightProbabilityOfPrecipitation; + } + + @SerializedName("dayProbabilityOfSnow") + private Double dayProbabilityOfSnow; + + public Double getDayProbabilityOfSnow() { + return dayProbabilityOfSnow; + } + + @SerializedName("nightProbabilityOfSnow") + private Double nightProbabilityOfSnow; + + public Double getNightProbabilityOfSnow() { + return nightProbabilityOfSnow; + } + + @SerializedName("dayProbabilityOfHeavySnow") + private Double dayProbabilityOfHeavySnow; + + public Double getDayProbabilityOfHeavySnow() { + return dayProbabilityOfHeavySnow; + } + + @SerializedName("nightProbabilityOfHeavySnow") + private Double nightProbabilityOfHeavySnow; + + public Double getNightProbabilityOfHeavySnow() { + return nightProbabilityOfHeavySnow; + } + + @SerializedName("dayProbabilityOfRain") + private Double dayProbabilityOfRain; + + public Double getDayProbabilityOfRain() { + return dayProbabilityOfRain; + } + + @SerializedName("nightProbabilityOfRain") + private Double nightProbabilityOfRain; + + public Double getNightProbabilityOfRain() { + return nightProbabilityOfRain; + } + + @SerializedName("dayProbabilityOfHeavyRain") + private Double dayProbabilityOfHeavyRain; + + public Double getDayProbabilityOfHeavyRain() { + return dayProbabilityOfHeavyRain; + } + + @SerializedName("nightProbabilityOfHeavyRain") + private Double nightProbabilityOfHeavyRain; + + public Double getNightProbabilityOfHeavyRain() { + return nightProbabilityOfHeavyRain; + } + + @SerializedName("dayProbabilityOfHail") + private Double dayProbabilityOfHail; + + public Double getDayProbabilityOfHail() { + return dayProbabilityOfHail; + } + + @SerializedName("nightProbabilityOfHail") + private Double nightProbabilityOfHail; + + public Double getNightProbabilityOfHail() { + return nightProbabilityOfHail; + } + + @SerializedName("dayProbabilityOfSferics") + private Double dayProbabilityOfSferics; + + public Double getDayProbabilityOfSferics() { + return dayProbabilityOfSferics; + } + + @SerializedName("nightProbabilityOfSferics") + private Double nightProbabilityOfSferics; + + public Double getNightProbabilityOfSferics() { + return nightProbabilityOfSferics; + } +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiTypedResponseObject.java b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiTypedResponseObject.java new file mode 100644 index 0000000000000..4188d159bb3ea --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/java/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiTypedResponseObject.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.dto.responses; + +import com.google.gson.annotations.SerializedName; + +/** + * The {@link SiteApiTypedResponseObject} is a Java class + * used as a base DTO where the response definitions can be + * identified by the type code embedded within the object. + * + * @author David Goodyear - Initial contribution + */ +public abstract class SiteApiTypedResponseObject { + @SerializedName("type") + private String type; + + public String getType() { + return type; + } + + public abstract String getExpectedType(); +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/addon/addon.xml b/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/addon/addon.xml new file mode 100644 index 0000000000000..7a884405626bb --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/addon/addon.xml @@ -0,0 +1,12 @@ + + + + binding + MetOfficeDataHub Binding + This is the binding for the Met Office DataHub service. + cloud + gb + + diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/i18n/metofficedatahub.properties b/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/i18n/metofficedatahub.properties new file mode 100644 index 0000000000000..b3fd4f1de7650 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/i18n/metofficedatahub.properties @@ -0,0 +1,319 @@ +# add-on + +addon.metofficedatahub.name = MetOfficeDataHub Binding +addon.metofficedatahub.description = This is the binding for the Met Office DataHub service. + +# thing types + +thing-type.metofficedatahub.account.label = MetOffice DataHub Account +thing-type.metofficedatahub.account.description = MetOffice DataHub API Account +thing-type.metofficedatahub.site.label = Forecast Data +thing-type.metofficedatahub.site.description = Site Specific forecast data +thing-type.metofficedatahub.site.group.current-forecast.label = Forecast Current Hour +thing-type.metofficedatahub.site.group.current-forecast.description = This is the weather forecast for the current hour. +thing-type.metofficedatahub.site.group.current-forecast-plus01.label = Forecast +1 Hour +thing-type.metofficedatahub.site.group.current-forecast-plus01.description = This is the weather forecast in 1 hour. +thing-type.metofficedatahub.site.group.current-forecast-plus02.label = Forecast +2 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus02.description = This is the weather forecast in 2 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus03.label = Forecast +3 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus03.description = This is the weather forecast in 3 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus04.label = Forecast +4 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus04.description = This is the weather forecast in 4 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus05.label = Forecast +5 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus05.description = This is the weather forecast in 5 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus06.label = Forecast +6 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus06.description = This is the weather forecast in 6 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus07.label = Forecast +7 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus07.description = This is the weather forecast in 7 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus08.label = Forecast +8 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus08.description = This is the weather forecast in 8 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus09.label = Forecast +9 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus09.description = This is the weather forecast in 9 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus10.label = Forecast +10 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus10.description = This is the weather forecast in 10 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus11.label = Forecast +11 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus11.description = This is the weather forecast in 11 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus12.label = Forecast +12 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus12.description = This is the weather forecast in 12 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus13.label = Forecast +13 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus13.description = This is the weather forecast in 13 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus14.label = Forecast +14 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus14.description = This is the weather forecast in 14 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus15.label = Forecast +15 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus15.description = This is the weather forecast in 15 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus16.label = Forecast +16 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus16.description = This is the weather forecast in 16 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus17.label = Forecast +17 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus17.description = This is the weather forecast in 17 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus18.label = Forecast +18 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus18.description = This is the weather forecast in 18 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus19.label = Forecast +19 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus19.description = This is the weather forecast in 19 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus20.label = Forecast +20 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus20.description = This is the weather forecast in 20 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus21.label = Forecast +21 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus21.description = This is the weather forecast in 21 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus22.label = Forecast +22 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus22.description = This is the weather forecast in 22 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus23.label = Forecast +23 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus23.description = This is the weather forecast in 23 hours. +thing-type.metofficedatahub.site.group.current-forecast-plus24.label = Forecast +24 Hours +thing-type.metofficedatahub.site.group.current-forecast-plus24.description = This is the weather forecast in 24 hours. +thing-type.metofficedatahub.site.group.daily-forecast.label = Forecast Current Day +thing-type.metofficedatahub.site.group.daily-forecast.description = This is the weather forecast for the current day. +thing-type.metofficedatahub.site.group.daily-forecast-plus01.label = Forecast +1 Day +thing-type.metofficedatahub.site.group.daily-forecast-plus01.description = This is the weather forecast in 1 day. +thing-type.metofficedatahub.site.group.daily-forecast-plus02.label = Forecast +2 Days +thing-type.metofficedatahub.site.group.daily-forecast-plus02.description = This is the weather forecast in 2 days. +thing-type.metofficedatahub.site.group.daily-forecast-plus03.label = Forecast +3 Days +thing-type.metofficedatahub.site.group.daily-forecast-plus03.description = This is the weather forecast in 3 days. +thing-type.metofficedatahub.site.group.daily-forecast-plus04.label = Forecast +4 Days +thing-type.metofficedatahub.site.group.daily-forecast-plus04.description = This is the weather forecast in 4 days. +thing-type.metofficedatahub.site.group.daily-forecast-plus05.label = Forecast +5 Days +thing-type.metofficedatahub.site.group.daily-forecast-plus05.description = This is the weather forecast in 5 days. +thing-type.metofficedatahub.site.group.daily-forecast-plus06.label = Forecast +6 Days +thing-type.metofficedatahub.site.group.daily-forecast-plus06.description = This is the weather forecast in 6 days. + +# thing types config + +thing-type.config.metofficedatahub.account.siteApiKey.label = Site Specific API Key +thing-type.config.metofficedatahub.account.siteApiKey.description = The API Key for the Site Specific subscription in your MET Office Data Hub account +thing-type.config.metofficedatahub.account.siteRateDailyLimit.label = API Daily Limit +thing-type.config.metofficedatahub.account.siteRateDailyLimit.description = A limit to the number of daily site specific API requests +thing-type.config.metofficedatahub.site.dailyForecastPollRate.label = Daily Poll Interval +thing-type.config.metofficedatahub.site.dailyForecastPollRate.description = The default number of hours to wait between retrieving daily forecast data +thing-type.config.metofficedatahub.site.hourlyForecastPollRate.label = Hourly Poll Interval +thing-type.config.metofficedatahub.site.hourlyForecastPollRate.description = The default number of hours to wait between retrieving hourly forecast data +thing-type.config.metofficedatahub.site.location.label = Weather Location +thing-type.config.metofficedatahub.site.location.description = Location of weather forecast in geographical coordinates (latitude/longitude). + +# channel group types + +channel-group-type.metofficedatahub.site-daily-forecast-grp.label = Daily Forecast +channel-group-type.metofficedatahub.site-daily-forecast-grp.description = Site Specific Location weather forecast - Daily basis +channel-group-type.metofficedatahub.site-hr-forecast-grp.label = Hourly Forecast +channel-group-type.metofficedatahub.site-hr-forecast-grp.description = Site Specific Location weather forecast - Hourly basis + +# channel types + +channel-type.metofficedatahub.air-temp-current-type.label = Temperature +channel-type.metofficedatahub.air-temp-current-type.description = Air Temperature +channel-type.metofficedatahub.air-temp-max-type.label = Max. Temperature +channel-type.metofficedatahub.air-temp-max-type.description = Maximum Screen Air Temperature Over Previous Hour +channel-type.metofficedatahub.air-temp-min-type.label = Min. Temperature +channel-type.metofficedatahub.air-temp-min-type.description = Minimum Screen Air Temperature Over Previous Hour +channel-type.metofficedatahub.day-prob-heavy-rain-type.label = Day H.Rain Probab. +channel-type.metofficedatahub.day-prob-heavy-rain-type.description = Probability of Heavy Rain During The Day - Heavy rain is defined as >1mm/hr. Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.dewpoint-type.label = Dew Point +channel-type.metofficedatahub.dewpoint-type.description = Dew Point Temperature +channel-type.metofficedatahub.feels-like-max-day-type.label = Day Feels Like Max. +channel-type.metofficedatahub.feels-like-max-day-type.description = Day Maximum Feels Like Air Temperature - This is the most likely maximum value over the day based on the ensemble spread. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.feels-like-max-lb-day-type.label = Day Max. Feels Like +channel-type.metofficedatahub.feels-like-max-lb-day-type.description = Lower Bound on Day Maximum Feels Like Air Temperature - This is the lower bound for the maximum value over the day based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.feels-like-max-ub-day-type.label = Day(UB) Feels Like Max. +channel-type.metofficedatahub.feels-like-max-ub-day-type.description = Upper Bound on Day Maximum Feels Like Air Temperature - This is the upper bound for the maximum value over the day based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% probability that the actual figure will be below this upper bound figure. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.feels-like-min-lb-night-type.label = Night(LB) Min. Feels Like +channel-type.metofficedatahub.feels-like-min-lb-night-type.description = Lower Bound on Night Minimum Feels Like Air Temperature - This is the lower bound for the minimum value over the night based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.feels-like-min-night-type.label = Night Feels Light Min. +channel-type.metofficedatahub.feels-like-min-night-type.description = Night Minimum Feels Like Air Temperature - This is the most likely minimum value over the night based on the ensemble spread. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.feels-like-min-ub-night-type.label = Night(UB) Feels Like Min. +channel-type.metofficedatahub.feels-like-min-ub-night-type.description = Upper Bound on Night Minimum Feels Like Air Temperature - This is the upper bound for the minimum value over the night based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% probability that the actual figure will be below this upper bound figure. This is the temperature it feels like taking into account humidity and wind chill but not radiation. Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.feels-like-type.label = Feels Like Temperature +channel-type.metofficedatahub.feels-like-type.description = Feels Like Temperature +channel-type.metofficedatahub.hail-prob-day-type.label = Day Hail Probab. +channel-type.metofficedatahub.hail-prob-day-type.description = Probability of Hail During The Day - Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.hail-prob-night-type.label = Night Hail Probab. +channel-type.metofficedatahub.hail-prob-night-type.description = Probability of Hail During The Night - Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.heavy-snow-prob-day-type.label = Day H.Snow Probab. +channel-type.metofficedatahub.heavy-snow-prob-day-type.description = Probability of Heavy Snow During The Day - Heavy snow is defined as >1mm/hr liquid water equivalent and is approximately equivilent to >1cm snow per hour. Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.heavy-snow-prob-night-type.label = Night H.Snow Probab. +channel-type.metofficedatahub.heavy-snow-prob-night-type.description = Probability of Heavy Snow During The Night - Heavy snow is defined as >1mm/hr liquid water equivalent and is approximately equivilent to >1cm snow per hour. Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.humidity-day-type.label = Midday Humidity +channel-type.metofficedatahub.humidity-day-type.description = Relative Humidity at Local Midday - Stevenson screen height is approximately 1.5m above ground level. +channel-type.metofficedatahub.humidity-night-type.label = Midnight Humidity +channel-type.metofficedatahub.humidity-night-type.description = Relative Humidity at Local Midnight - Stevenson screen height is approximately 1.5m above ground level. +channel-type.metofficedatahub.humidity-type.label = Relative Humidity +channel-type.metofficedatahub.humidity-type.description = Screen Relative Humidity +channel-type.metofficedatahub.loc-name-type.label = Location Name +channel-type.metofficedatahub.loc-name-type.description = Name of location represented +channel-type.metofficedatahub.night-prob-heavy-rain-type.label = Night H.Rain Probab. +channel-type.metofficedatahub.night-prob-heavy-rain-type.description = Probability of Heavy Rain During The Night - Heavy rain is defined as >1mm/hr. Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.nightLowerBoundMaxTemp.label = Day(LB) Max. Temperature +channel-type.metofficedatahub.nightLowerBoundMaxTemp.description = Lower Bound on Day Maximum Screen Air Temperature - This is the lower bound for the minimum value over the day based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. Stevenson screen height is approximately 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.precip-prob-day-type.label = Day Precip. Probab. +channel-type.metofficedatahub.precip-prob-day-type.description = Probability of Precipitation During The Day - Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.precip-prob-night-type.label = Night Precip. Probab. +channel-type.metofficedatahub.precip-prob-night-type.description = Probability of Precipitation During The Night - Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.precip-prob-type.label = Precip. Probability +channel-type.metofficedatahub.precip-prob-type.description = Probability of Precipitation +channel-type.metofficedatahub.precip-rate-type.label = Precipitation Rate +channel-type.metofficedatahub.precip-rate-type.description = Precipitation Rate +channel-type.metofficedatahub.precip-total-type.label = Previous Hour Precip. +channel-type.metofficedatahub.precip-total-type.description = Total Precipitation Amount Over Previous Hour +channel-type.metofficedatahub.pressure-day-type.label = Midday Pressure +channel-type.metofficedatahub.pressure-day-type.description = Mean Sea Level Pressure at Local Midnight - Air pressure at mean sea level which is close to the geoid in sea areas. Air pressure at sea level is the quantity often abbreviated as pressure or PMSL. +channel-type.metofficedatahub.pressure-night-type.label = Midnight Pressure +channel-type.metofficedatahub.pressure-night-type.description = Mean Sea Level Pressure at Local Midnight - Air pressure at mean sea level which is close to the geoid in sea areas. Air pressure at sea level is the quantity often abbreviated as pressure or PMSL. +channel-type.metofficedatahub.pressure-type.label = Pressure +channel-type.metofficedatahub.pressure-type.description = Mean Sea Level Pressure +channel-type.metofficedatahub.rain-prob-day-type.label = Day Rain Probab. +channel-type.metofficedatahub.rain-prob-day-type.description = Probability of Rain During The Day - Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.rain-prob-night-type.label = Night Rain Probab. +channel-type.metofficedatahub.rain-prob-night-type.description = Probability of Rain During The Night - Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.sferics-prob-day-type.label = Day Sferics Probab. +channel-type.metofficedatahub.sferics-prob-day-type.description = Probability of Sferics During The Day - This is the probability of a strike within a radius of 50km. +channel-type.metofficedatahub.sferics-prob-night-type.label = Night Sferics Probab. +channel-type.metofficedatahub.sferics-prob-night-type.description = Probability of Sferics During The Night - This is the probability of a strike within a radius of 50km. +channel-type.metofficedatahub.snow-prob-day-type.label = Day Snow Probab. +channel-type.metofficedatahub.snow-prob-day-type.description = Probability of Snow During The Day - Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.snow-prob-night-type.label = Night Snow Probab. +channel-type.metofficedatahub.snow-prob-night-type.description = Probability of Snow During The Night - Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.snow-total-type.label = Previous Hour Snowfall +channel-type.metofficedatahub.snow-total-type.description = Total Snowfall Amount Over Previous Hour +channel-type.metofficedatahub.temp-max-day-type.label = Day Max. Temperature +channel-type.metofficedatahub.temp-max-day-type.description = Day Maximum Screen Air Temperature - Daytime is defined as those forecast times that fall between local dawn and dusk +channel-type.metofficedatahub.temp-max-lb-day-type.label = Day(LB) Max. Temperature +channel-type.metofficedatahub.temp-max-lb-day-type.description = Lower Bound on Day Maximum Screen Air Temperature - This is the lower bound for the maximum value over the day based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. Stevenson screen height is approximately 1.5m above ground level. Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.temp-max-ub-day-type.label = Day(UB) Max. Temperature +channel-type.metofficedatahub.temp-max-ub-day-type.description = Upper Bound on Day Maximum Screen Air Temperature - This is the upper bound for the maximum value over the day based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% probability that the actual figure will be below this upper bound figure. Stevenson screen height is approximately 1.5m above ground level. Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.temp-min-lb-night-type.label = Night(LB) Min. Temperature +channel-type.metofficedatahub.temp-min-lb-night-type.description = Lower Bound on Night Minimum Screen Air Temperature - This is the lower bound for the minimum value over the night based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% probability that the actual figure will be above this lower bound figure. Stevenson screen height is approximately 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.temp-min-night-type.label = Night Min. Temperature +channel-type.metofficedatahub.temp-min-night-type.description = Night Minimum Screen Air Temperature - Night-time is defined as those forecast times that fall between local dusk and dawn +channel-type.metofficedatahub.temp-min-ub-night-type.label = Night(UB) Min. Temperature +channel-type.metofficedatahub.temp-min-ub-night-type.description = Upper Bound on Night Minimum Screen Air Temperature - This is the upper bound for the minimum value over the night based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% probability that the actual figure will be below this upper bound figure. Stevenson screen height is approximately 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. +channel-type.metofficedatahub.time-type.label = Forecast Time Start +channel-type.metofficedatahub.time-type.description = Time of forecast time window start +channel-type.metofficedatahub.time-type.state.pattern = %1$tF %1$tR +channel-type.metofficedatahub.uv-index-type.label = UV Index +channel-type.metofficedatahub.uv-index-type.description = UV Index +channel-type.metofficedatahub.uv-max-type.label = Day Max. UV +channel-type.metofficedatahub.uv-max-type.description = Day Maximum UV Index - Usually a value from 0 to 13 but higher values are possible in extreme situations. Daytime is defined as those forecast times that fall between local dawn and dusk. +channel-type.metofficedatahub.visibility-day-type.label = Midday Visibility +channel-type.metofficedatahub.visibility-day-type.description = Visibility at Local Midday - Minimal horizontal distance at which a known object can be seen. +channel-type.metofficedatahub.visibility-night-type.label = Midnight Visibility +channel-type.metofficedatahub.visibility-night-type.description = Visibility at Local Midnight - Minimal horizontal distance at which a known object can be seen. +channel-type.metofficedatahub.visibility-type.label = Visibility +channel-type.metofficedatahub.visibility-type.description = Visibility +channel-type.metofficedatahub.wind-direction-day-type.label = Midday Wind Direction +channel-type.metofficedatahub.wind-direction-day-type.description = 10m Wind Direction at Local Midday - Mean wind direction is equivalent to the mean direction observed over the 10 minutes preceding the validity time. In meteorological reports the direction of the wind vector is given as the direction from which it is blowing. 10m wind is the considered surface wind. +channel-type.metofficedatahub.wind-direction-night-type.label = Midnight Wind Direction +channel-type.metofficedatahub.wind-direction-night-type.description = 10m Wind Direction at Local Midnight - Mean wind direction is equivalent to the mean direction observed over the 10 minutes preceding the validity time. In meteorological reports the direction of the wind vector is given as the direction from which it is blowing. 10m wind is the considered surface wind. +channel-type.metofficedatahub.wind-direction-type.label = Wind From +channel-type.metofficedatahub.wind-direction-type.description = 10m Wind From Direction +channel-type.metofficedatahub.wind-gust-day-type.label = Midday Wind Gust +channel-type.metofficedatahub.wind-gust-day-type.description = 10m Wind Gust Speed at Local Midday - The gust speed is equivalent to the maximum 3 second mean wind speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. +channel-type.metofficedatahub.wind-gust-max-type.label = Max Wind Gust Prev.Hr +channel-type.metofficedatahub.wind-gust-max-type.description = Maximum 10m Wind Gust Speed of Previous Hour +channel-type.metofficedatahub.wind-gust-night-type.label = Midnight Wind Gust +channel-type.metofficedatahub.wind-gust-night-type.description = 10m Wind Gust Speed at Local Midnight - The gust speed is equivalent to the maximum 3 second mean wind speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. +channel-type.metofficedatahub.wind-speed-day-type.label = Midday Wind Speed +channel-type.metofficedatahub.wind-speed-day-type.description = 10m Wind Speed at Local Midday - Mean wind speed is equivalent to the mean speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. +channel-type.metofficedatahub.wind-speed-gust-type.label = Wind Gust +channel-type.metofficedatahub.wind-speed-gust-type.description = 10m Wind Gust Speed +channel-type.metofficedatahub.wind-speed-night-type.label = Midnight Wind Speed +channel-type.metofficedatahub.wind-speed-night-type.description = 10m Wind Speed at Local Midnight - Mean wind speed is equivalent to the mean speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. +channel-type.metofficedatahub.wind-speed-type.label = Wind Speed +channel-type.metofficedatahub.wind-speed-type.description = 10m Wind Speed + +# thing types + +thing-type.metofficedatahub.site.group.common-data.label = Common Forecast Data +thing-type.metofficedatahub.site.group.common-data.description = This is common data to all forecast data. + +# channel group types + +channel-group-type.metofficedatahub.site-common-data-grp.label = Common Site Data +channel-group-type.metofficedatahub.site-common-data-grp.description = Site Specific Location weather forecast - Common Data + +# thing types + +thing-type.metofficedatahub.siteSpecificApi.label = Forecast Data +thing-type.metofficedatahub.siteSpecificApi.description = Site Specific forecast data +thing-type.metofficedatahub.siteSpecificApi.group.common-data.label = Common Forecast Data +thing-type.metofficedatahub.siteSpecificApi.group.common-data.description = This is common data to all forecast data. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast.label = Forecast Current Hour +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast.description = This is the weather forecast for the current hour. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus01.label = Forecast +1 Hour +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus01.description = This is the weather forecast in 1 hour. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus02.label = Forecast +2 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus02.description = This is the weather forecast in 2 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus03.label = Forecast +3 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus03.description = This is the weather forecast in 3 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus04.label = Forecast +4 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus04.description = This is the weather forecast in 4 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus05.label = Forecast +5 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus05.description = This is the weather forecast in 5 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus06.label = Forecast +6 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus06.description = This is the weather forecast in 6 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus07.label = Forecast +7 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus07.description = This is the weather forecast in 7 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus08.label = Forecast +8 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus08.description = This is the weather forecast in 8 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus09.label = Forecast +9 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus09.description = This is the weather forecast in 9 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus10.label = Forecast +10 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus10.description = This is the weather forecast in 10 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus11.label = Forecast +11 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus11.description = This is the weather forecast in 11 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus12.label = Forecast +12 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus12.description = This is the weather forecast in 12 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus13.label = Forecast +13 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus13.description = This is the weather forecast in 13 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus14.label = Forecast +14 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus14.description = This is the weather forecast in 14 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus15.label = Forecast +15 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus15.description = This is the weather forecast in 15 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus16.label = Forecast +16 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus16.description = This is the weather forecast in 16 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus17.label = Forecast +17 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus17.description = This is the weather forecast in 17 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus18.label = Forecast +18 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus18.description = This is the weather forecast in 18 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus19.label = Forecast +19 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus19.description = This is the weather forecast in 19 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus20.label = Forecast +20 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus20.description = This is the weather forecast in 20 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus21.label = Forecast +21 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus21.description = This is the weather forecast in 21 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus22.label = Forecast +22 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus22.description = This is the weather forecast in 22 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus23.label = Forecast +23 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus23.description = This is the weather forecast in 23 hours. +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus24.label = Forecast +24 Hours +thing-type.metofficedatahub.siteSpecificApi.group.current-forecast-plus24.description = This is the weather forecast in 24 hours. +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast.label = Forecast Current Day +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast.description = This is the weather forecast for the current day. +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus01.label = Forecast +1 Day +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus01.description = This is the weather forecast in 1 day. +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus02.label = Forecast +2 Days +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus02.description = This is the weather forecast in 2 days. +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus03.label = Forecast +3 Days +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus03.description = This is the weather forecast in 3 days. +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus04.label = Forecast +4 Days +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus04.description = This is the weather forecast in 4 days. +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus05.label = Forecast +5 Days +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus05.description = This is the weather forecast in 5 days. +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus06.label = Forecast +6 Days +thing-type.metofficedatahub.siteSpecificApi.group.daily-forecast-plus06.description = This is the weather forecast in 6 days. + +# thing types config + +thing-type.config.metofficedatahub.siteSpecificApi.dailyForecastPollRate.label = Daily Poll Cycle +thing-type.config.metofficedatahub.siteSpecificApi.dailyForecastPollRate.description = The default number of hours to wait between retrieving daily forecast data +thing-type.config.metofficedatahub.siteSpecificApi.hourlyForecastPollRate.label = Hourly Poll Cycle +thing-type.config.metofficedatahub.siteSpecificApi.hourlyForecastPollRate.description = The default number of hours to wait between retrieving hourly forecast data +thing-type.config.metofficedatahub.siteSpecificApi.location.label = Weather Location +thing-type.config.metofficedatahub.siteSpecificApi.location.description = Location of weather forecast in geographical coordinates (latitude/longitude). + +# channel types + +api.log.rate-limiter.failed-restore = Limiter {0} -> Did not load limiter data due to : {1} +bridge.error.site-specific.auth-issue = Check siteSpecificApiKey is correct in account - authentication failure +bridge.error.site-specific.communication-failure = Communications failure due to : {0} +bridge.error.site-specific.communication-failure.unknown = unknown reason +site.error.no-bridge = Disabled requesting data - this things Bridge is not set +site.error.no-user-location = Missing openHAB's user configured location +site.error.invalid-location = Invalid location given +site.error.not-enough-data = No data cached or retrieved recently enough +comm.comm-check.no-quota-left = Request limit reached - cannot check connectivity to Met Office - presuming online diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 0000000000000..a0c0839ed9429 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,615 @@ + + + + + + + Site Specific Location weather forecast - Hourly basis + + + + + + + + + + + + + + + + + + + + + + + + + Site Specific Location weather forecast - Daily basis + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + Name of location represented + + + + + DateTime + + Time of forecast time window start + Time + + + + + Number:Temperature + + Air Temperature + Temperature + + + + + Number:Temperature + + Minimum Screen Air Temperature Over Previous Hour + Temperature + + + + + Number:Temperature + + Maximum Screen Air Temperature Over Previous Hour + Temperature + + + + + Number:Temperature + + Feels Like Temperature + Temperature + + + + + Number:Dimensionless + + Screen Relative Humidity + Humidity + + + + + Number:Length + + Visibility + Sun_Clouds + + + + + Number:Dimensionless + + Probability of Precipitation + Rain + + + + + Number:Speed + + Precipitation Rate + Rain + + + + + Number:Length + + Total Precipitation Amount Over Previous Hour + Rain + + + + + Number:Length + + Total Snowfall Amount Over Previous Hour + Snow + + + + + Number:Dimensionless + + UV Index + Sun + + + + + Number:Pressure + + Mean Sea Level Pressure + Pressure + + + + + Number:Speed + + 10m Wind Speed + Wind + + + + + Number:Speed + + 10m Wind Gust Speed + Wind + + + + + Number:Speed + + Maximum 10m Wind Gust Speed of Previous Hour + Wind + + + + + Number:Temperature + + Dew Point Temperature + Temperature + + + + + Number:Angle + + 10m Wind From Direction + Wind + + + + + + Number:Speed + + 10m Wind Speed at Local Midday - Mean wind speed is equivalent to the mean speed observed over the 10 + minutes preceding the validity time. 10m wind is the considered surface wind. + Wind + + + + + Number:Speed + + 10m Wind Speed at Local Midnight - Mean wind speed is equivalent to the mean speed observed over the 10 + minutes preceding the validity time. 10m wind is the considered surface wind. + Wind + + + + + Number:Angle + + 10m Wind Direction at Local Midday - Mean wind direction is equivalent to the mean direction observed + over the 10 minutes preceding the validity time. In meteorological reports the direction of the wind vector is given + as the direction from which it is blowing. 10m wind is the considered surface wind. + Wind + + + + + Number:Angle + + 10m Wind Direction at Local Midnight - Mean wind direction is equivalent to the mean direction observed + over the 10 minutes preceding the validity time. In meteorological reports the direction of the wind vector is given + as the direction from which it is blowing. 10m wind is the considered surface wind. + Wind + + + + + Number:Speed + + 10m Wind Gust Speed at Local Midday - The gust speed is equivalent to the maximum 3 second mean wind + speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. + Wind + + + + + Number:Speed + + 10m Wind Gust Speed at Local Midnight - The gust speed is equivalent to the maximum 3 second mean wind + speed observed over the 10 minutes preceding the validity time. 10m wind is the considered surface wind. + Wind + + + + + Number:Length + + Visibility at Local Midday - Minimal horizontal distance at which a known object can be seen. + Sun_Clouds + + + + + Number:Length + + Visibility at Local Midnight - Minimal horizontal distance at which a known object can be seen. + Sun_Clouds + + + + + Number:Dimensionless + + Relative Humidity at Local Midday - Stevenson screen height is approximately 1.5m above ground level. + Humidity + + + + + Number:Dimensionless + + Relative Humidity at Local Midnight - Stevenson screen height is approximately 1.5m above ground level. + Humidity + + + + + Number:Pressure + + Mean Sea Level Pressure at Local Midnight - Air pressure at mean sea level which is close to the geoid in + sea areas. Air pressure at sea level is the quantity often abbreviated as pressure or PMSL. + Pressure + + + + + Number:Pressure + + Mean Sea Level Pressure at Local Midnight - Air pressure at mean sea level which is close to the geoid in + sea areas. Air pressure at sea level is the quantity often abbreviated as pressure or PMSL. + Pressure + + + + + Number:Dimensionless + + Day Maximum UV Index - Usually a value from 0 to 13 but higher values are possible in extreme situations. + Daytime is defined as those forecast times that fall between local dawn and dusk. + Sun + + + + + Number:Temperature + + Upper Bound on Day Maximum Screen Air Temperature - This is the upper bound for the maximum value over + the day based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% + probability that the actual figure will be below this upper bound figure. Stevenson screen height is approximately + 1.5m above ground level. Daytime is defined as those forecast times that fall between local dawn and dusk. + Temperature + + + + + Number:Temperature + + Upper Bound on Night Minimum Screen Air Temperature - This is the upper bound for the minimum value over + the night based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% + probability that the actual figure will be below this upper bound figure. Stevenson screen height is approximately + 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. + Temperature + + + + + Number:Temperature + + Lower Bound on Day Maximum Screen Air Temperature - This is the lower bound for the maximum value over + the day based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% + probability that the actual figure will be above this lower bound figure. Stevenson screen height is approximately + 1.5m above ground level. Daytime is defined as those forecast times that fall between local dawn and dusk. + Temperature + + + + + Number:Temperature + + Lower Bound on Day Maximum Screen Air Temperature - This is the lower bound for the minimum value over + the day based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% + probability that the actual figure will be above this lower bound figure. Stevenson screen height is approximately + 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. + Temperature + + + + + Number:Temperature + + Lower Bound on Night Minimum Screen Air Temperature - This is the lower bound for the minimum value over + the night based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% + probability that the actual figure will be above this lower bound figure. Stevenson screen height is approximately + 1.5m above ground level. Night-time is defined as those forecast times that fall between local dusk and dawn. + Temperature + + + + + Number:Temperature + + Day Maximum Feels Like Air Temperature - This is the most likely maximum value over the day based on the + ensemble spread. This is the temperature it feels like taking into account humidity and wind chill but not radiation. + Daytime is defined as those forecast times that fall between local dawn and dusk. + Temperature + + + + + Number:Temperature + + Night Minimum Feels Like Air Temperature - This is the most likely minimum value over the night based on + the ensemble spread. This is the temperature it feels like taking into account humidity and wind chill but not + radiation. Night-time is defined as those forecast times that fall between local dusk and dawn. + Temperature + + + + + Number:Temperature + + Upper Bound on Day Maximum Feels Like Air Temperature - This is the upper bound for the maximum value + over the day based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% + probability that the actual figure will be below this upper bound figure. This is the temperature it feels like + taking into account humidity and wind chill but not radiation. Daytime is defined as those forecast times that fall + between local dawn and dusk. + Temperature + + + + + Number:Temperature + + Upper Bound on Night Minimum Feels Like Air Temperature - This is the upper bound for the minimum value + over the night based on the ensemble spread. It is actually given by the 97.5 percentile. This means there is a 97.5% + probability that the actual figure will be below this upper bound figure. This is the temperature it feels like + taking into account humidity and wind chill but not radiation. Night-time is defined as those forecast times that + fall between local dusk and dawn. + Temperature + + + + + Number:Temperature + + Lower Bound on Day Maximum Feels Like Air Temperature - This is the lower bound for the maximum value + over the day based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% + probability that the actual figure will be above this lower bound figure. This is the temperature it feels like + taking into account humidity and wind chill but not radiation. Daytime is defined as those forecast times that fall + between local dawn and dusk. + Temperature + + + + + Number:Temperature + + Lower Bound on Night Minimum Feels Like Air Temperature - This is the lower bound for the minimum value + over the night based on the ensemble spread. It is actually given by the 2.5 percentile. This means there is a 97.5% + probability that the actual figure will be above this lower bound figure. This is the temperature it feels like + taking into account humidity and wind chill but not radiation. Night-time is defined as those forecast times that + fall between local dusk and dawn. + Temperature + + + + + Number:Dimensionless + + Probability of Precipitation During The Day - Daytime is defined as those forecast times that fall + between local dawn and dusk. + Rain + + + + + Number:Dimensionless + + Probability of Precipitation During The Night - Night-time is defined as those forecast times that fall + between local dusk and dawn. + Rain + + + + + Number:Dimensionless + + Probability of Snow During The Day - Daytime is defined as those forecast times that fall between local + dawn and dusk. + Rain + + + + + Number:Dimensionless + + Probability of Snow During The Night - Night-time is defined as those forecast times that fall between + local dusk and dawn. + Rain + + + + + Number:Dimensionless + + Probability of Heavy Snow During The Day - Heavy snow is defined as >1mm/hr liquid water equivalent + and is approximately equivilent to >1cm snow per hour. Daytime is defined as those forecast times that fall + between local dawn and dusk. + Rain + + + + + Number:Dimensionless + + Probability of Heavy Snow During The Night - Heavy snow is defined as >1mm/hr liquid water equivalent + and is approximately equivilent to >1cm snow per hour. Night-time is defined as those forecast times that fall + between local dusk and dawn. + Rain + + + + + Number:Dimensionless + + Probability of Rain During The Day - Daytime is defined as those forecast times that fall between local + dawn and dusk. + Rain + + + + + Number:Dimensionless + + Probability of Rain During The Night - Night-time is defined as those forecast times that fall between + local dusk and dawn. + Rain + + + + + Number:Dimensionless + + Probability of Heavy Rain During The Day - Heavy rain is defined as >1mm/hr. Daytime is defined as + those forecast times that fall between local dawn and dusk. + Rain + + + + + Number:Dimensionless + + Probability of Heavy Rain During The Night - Heavy rain is defined as >1mm/hr. Night-time is defined + as those forecast times that fall between local dusk and dawn. + Rain + + + + + Number:Dimensionless + + Probability of Hail During The Day - Daytime is defined as those forecast times that fall between local + dawn and dusk. + Rain + + + + + Number:Dimensionless + + Probability of Hail During The Night - Night-time is defined as those forecast times that fall between + local dusk and dawn. + Rain + + + + + Number:Dimensionless + + Probability of Sferics During The Day - This is the probability of a strike within a radius of 50km. + Rain + + + + + Number:Dimensionless + + Probability of Sferics During The Night - This is the probability of a strike within a radius of 50km. + Rain + + + + + Number:Temperature + + Day Maximum Screen Air Temperature - Daytime is defined as those forecast times that fall between local + dawn and dusk + Temperature + + + + + Number:Temperature + + Night Minimum Screen Air Temperature - Night-time is defined as those forecast times that fall between + local dusk and dawn + Temperature + + + + diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..9466e96d7d57d --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,193 @@ + + + + + + + MetOffice DataHub API Account + + + + + + + + password + + The API Key for the Site Specific subscription in your MET Office Data Hub account + + + + A limit to the number of daily site specific API requests + 250 + true + + + + + + + + + + + + Site Specific forecast data + + + + + This is the weather forecast for the current hour. + + + + This is the weather forecast in 1 hour. + + + + This is the weather forecast in 2 hours. + + + + This is the weather forecast in 3 hours. + + + + This is the weather forecast in 4 hours. + + + + This is the weather forecast in 5 hours. + + + + This is the weather forecast in 6 hours. + + + + This is the weather forecast in 7 hours. + + + + This is the weather forecast in 8 hours. + + + + This is the weather forecast in 9 hours. + + + + This is the weather forecast in 10 hours. + + + + This is the weather forecast in 11 hours. + + + + This is the weather forecast in 12 hours. + + + + This is the weather forecast in 13 hours. + + + + This is the weather forecast in 14 hours. + + + + This is the weather forecast in 15 hours. + + + + This is the weather forecast in 16 hours. + + + + This is the weather forecast in 17 hours. + + + + This is the weather forecast in 18 hours. + + + + This is the weather forecast in 19 hours. + + + + This is the weather forecast in 20 hours. + + + + This is the weather forecast in 21 hours. + + + + This is the weather forecast in 22 hours. + + + + This is the weather forecast in 23 hours. + + + + This is the weather forecast in 24 hours. + + + + This is the weather forecast for the current day. + + + + This is the weather forecast in 1 day. + + + + This is the weather forecast in 2 days. + + + + This is the weather forecast in 3 days. + + + + This is the weather forecast in 4 days. + + + + This is the weather forecast in 5 days. + + + + This is the weather forecast in 6 days. + + + + + + location + + Location of weather forecast in geographical coordinates (latitude/longitude). + + + + The default number of hours to wait between retrieving hourly forecast data + 1 + true + + + + The default number of hours to wait between retrieving daily forecast data + 3 + true + + + + + + diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/api/SiteApiAuthenticationTest.java b/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/api/SiteApiAuthenticationTest.java new file mode 100644 index 0000000000000..5bb6273d9bb1b --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/api/SiteApiAuthenticationTest.java @@ -0,0 +1,93 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.api; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.HttpResponse; +import org.eclipse.jetty.client.api.Request; +import org.eclipse.jetty.client.api.Response; +import org.eclipse.jetty.client.api.Result; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The {@link SiteApiAuthenticationTest} class implements unit test case for {@link SiteApiAuthenticationTest} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class SiteApiAuthenticationTest { + + private static Request dummyRequest = new HttpClient().newRequest("http://127.0.0.1:9999");; + + @Test + public void testInitialNonAuthenticatedState() { + SiteApiAuthentication saa = new SiteApiAuthentication(); + assertFalse(saa.getIsAuthenticated()); + } + + @Test + public void testAuthenticatedProcessing() { + SiteApiAuthentication saa = new SiteApiAuthentication(); + Result result = new Result(dummyRequest,getResultWithStatus(200)); + saa.processResult(result); + assertTrue(saa.getIsAuthenticated()); + } + + @Test + public void testUnauthenticatedProcessing() { + SiteApiAuthentication saa = new SiteApiAuthentication(); + Result result = new Result(dummyRequest,getResultWithStatus(403)); + saa.processResult(result); + assertFalse(saa.getIsAuthenticated()); + } + + @Test + public void testIsAuthenticatedUpdates() { + SiteApiAuthentication saa = new SiteApiAuthentication(); + Result goodResult = new Result(dummyRequest,getResultWithStatus(200)); + Result badresult = new Result(dummyRequest,getResultWithStatus(403)); + saa.processResult(goodResult); + assertTrue(saa.getIsAuthenticated()); + saa.processResult(badresult); + assertFalse(saa.getIsAuthenticated()); + saa.processResult(goodResult); + assertTrue(saa.getIsAuthenticated()); + } + + @Test + public void testBadJwtDetected() { + SiteApiAuthentication saa = new SiteApiAuthentication(); + assertThrowsExactly(AuthTokenException.class, () -> { saa.setApiKey("");}); + assertThrowsExactly(AuthTokenException.class, () -> { saa.setApiKey("someInvalidToken.part");}); + assertThrowsExactly(AuthTokenException.class, () -> { saa.setApiKey("\"someInvalidToken.part.new");}); + } + + @Test + public void testGoodJwtDetected() { + SiteApiAuthentication saa = new SiteApiAuthentication(); + assertDoesNotThrow(() -> { saa.setApiKey("eyJhbGciOiJIUzI1NiJ9.eyJSb2xlIjoiQWRtaW4iLCJJc3N1ZXIiOiJJc3N1ZXIiLCJVc2VybmFtZSI6IkphdmFJblVzZSIsImV4cCI6MTczMDg1Mzg2NiwiaWF0IjoxNzMwODUzODY2fQ.Wmfe4npC037y0uoW4dnhizSXOPqFSn3OI3XbeklVQkA");}); + } + + private Response getResultWithStatus(final int status) { + return new HttpResponse(dummyRequest, List.of()).status(status); + } + +} diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/2022-09-siteDailyResponse.json b/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/2022-09-siteDailyResponse.json new file mode 100644 index 0000000000000..b89719747a963 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/2022-09-siteDailyResponse.json @@ -0,0 +1,811 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.32430000000000003, + 51.0624, + 50.0 + ] + }, + "properties": { + "location": { + "name": "Horsham" + }, + "requestPointDistance": 0.1508, + "modelRunDate": "2022-09-19T21:00Z", + "timeSeries": [ + { + "time": "2022-09-18T00:00Z", + "midnight10MWindSpeed": 1.39, + "midnight10MWindDirection": 31, + "midnight10MWindGust": 5.66, + "midnightVisibility": 8776, + "midnightRelativeHumidity": 91.42, + "midnightMslp": 102310, + "nightSignificantWeatherCode": 2, + "nightMinScreenTemperature": 8.05, + "nightUpperBoundMinTemp": 12.79, + "nightLowerBoundMinTemp": 5.51, + "nightMinFeelsLikeTemp": 7.35, + "nightUpperBoundMinFeelsLikeTemp": 12.83, + "nightLowerBoundMinFeelsLikeTemp": 7.33, + "nightProbabilityOfPrecipitation": 5, + "nightProbabilityOfSnow": 0, + "nightProbabilityOfHeavySnow": 1, + "nightProbabilityOfRain": 5, + "nightProbabilityOfHeavyRain": 3, + "nightProbabilityOfHail": 4, + "nightProbabilityOfSferics": 6 + }, + { + "time": "2022-09-19T00:00Z", + "midday10MWindSpeed": 1.03, + "midnight10MWindSpeed": 1.81, + "midday10MWindDirection": 299, + "midnight10MWindDirection": 323, + "midday10MWindGust": 2.06, + "midnight10MWindGust": 4.05, + "middayVisibility": 28034, + "midnightVisibility": 20277, + "middayRelativeHumidity": 63.54, + "midnightRelativeHumidity": 80.83, + "middayMslp": 102519, + "midnightMslp": 102541, + "maxUvIndex": 3, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 17.64, + "nightMinScreenTemperature": 10.93, + "dayUpperBoundMaxTemp": 18.51, + "nightUpperBoundMinTemp": 13.74, + "dayLowerBoundMaxTemp": 14.22, + "nightLowerBoundMinTemp": 6.96, + "dayMaxFeelsLikeTemp": 17.06, + "nightMinFeelsLikeTemp": 11.12, + "dayUpperBoundMaxFeelsLikeTemp": 17.87, + "nightUpperBoundMinFeelsLikeTemp": 13.75, + "dayLowerBoundMaxFeelsLikeTemp": 14.57, + "nightLowerBoundMinFeelsLikeTemp": 9.81, + "dayProbabilityOfPrecipitation": 9, + "nightProbabilityOfPrecipitation": 6, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 9, + "nightProbabilityOfRain": 6, + "dayProbabilityOfHeavyRain": 0, + "nightProbabilityOfHeavyRain": 1, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 0 + }, + { + "time": "2022-09-20T00:00Z", + "midday10MWindSpeed": 1.55, + "midnight10MWindSpeed": 1.02, + "midday10MWindDirection": 10, + "midnight10MWindDirection": 304, + "midday10MWindGust": 3.11, + "midnight10MWindGust": 2.33, + "middayVisibility": 28838, + "midnightVisibility": 16314, + "middayRelativeHumidity": 51.07, + "midnightRelativeHumidity": 83.33, + "middayMslp": 102680, + "midnightMslp": 102713, + "maxUvIndex": 3, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 2, + "dayMaxScreenTemperature": 18.09, + "nightMinScreenTemperature": 9.92, + "dayUpperBoundMaxTemp": 19.61, + "nightUpperBoundMinTemp": 14.7, + "dayLowerBoundMaxTemp": 16.79, + "nightLowerBoundMinTemp": 5.69, + "dayMaxFeelsLikeTemp": 16.98, + "nightMinFeelsLikeTemp": 9.91, + "dayUpperBoundMaxFeelsLikeTemp": 18.95, + "nightUpperBoundMinFeelsLikeTemp": 14.99, + "dayLowerBoundMaxFeelsLikeTemp": 15.98, + "nightLowerBoundMinFeelsLikeTemp": 8.69, + "dayProbabilityOfPrecipitation": 5, + "nightProbabilityOfPrecipitation": 4, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 5, + "nightProbabilityOfRain": 4, + "dayProbabilityOfHeavyRain": 0, + "nightProbabilityOfHeavyRain": 0, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 0 + }, + { + "time": "2022-09-21T00:00Z", + "midday10MWindSpeed": 1.05, + "midnight10MWindSpeed": 1.6, + "midday10MWindDirection": 201, + "midnight10MWindDirection": 133, + "midday10MWindGust": 2.85, + "midnight10MWindGust": 2.7, + "middayVisibility": 28467, + "midnightVisibility": 20397, + "middayRelativeHumidity": 51.33, + "midnightRelativeHumidity": 90.57, + "middayMslp": 102640, + "midnightMslp": 102440, + "maxUvIndex": 4, + "daySignificantWeatherCode": 1, + "nightSignificantWeatherCode": 2, + "dayMaxScreenTemperature": 19.75, + "nightMinScreenTemperature": 9.04, + "dayUpperBoundMaxTemp": 20.85, + "nightUpperBoundMinTemp": 12.07, + "dayLowerBoundMaxTemp": 17.71, + "nightLowerBoundMinTemp": 5.93, + "dayMaxFeelsLikeTemp": 18.82, + "nightMinFeelsLikeTemp": 8.75, + "dayUpperBoundMaxFeelsLikeTemp": 20.07, + "nightUpperBoundMinFeelsLikeTemp": 11.3, + "dayLowerBoundMaxFeelsLikeTemp": 17.23, + "nightLowerBoundMinFeelsLikeTemp": 5.31, + "dayProbabilityOfPrecipitation": 1, + "nightProbabilityOfPrecipitation": 2, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 1, + "nightProbabilityOfRain": 2, + "dayProbabilityOfHeavyRain": 0, + "nightProbabilityOfHeavyRain": 0, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 0 + }, + { + "time": "2022-09-22T00:00Z", + "midday10MWindSpeed": 3.87, + "midnight10MWindSpeed": 2.21, + "midday10MWindDirection": 192, + "midnight10MWindDirection": 167, + "midday10MWindGust": 8.14, + "midnight10MWindGust": 3.35, + "middayVisibility": 32675, + "midnightVisibility": 25210, + "middayRelativeHumidity": 61.39, + "midnightRelativeHumidity": 88.29, + "middayMslp": 102164, + "midnightMslp": 101816, + "maxUvIndex": 3, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 7, + "dayMaxScreenTemperature": 18.71, + "nightMinScreenTemperature": 10.89, + "dayUpperBoundMaxTemp": 21.93, + "nightUpperBoundMinTemp": 14.6, + "dayLowerBoundMaxTemp": 17.77, + "nightLowerBoundMinTemp": 7.75, + "dayMaxFeelsLikeTemp": 16.78, + "nightMinFeelsLikeTemp": 10.78, + "dayUpperBoundMaxFeelsLikeTemp": 19.63, + "nightUpperBoundMinFeelsLikeTemp": 13.97, + "dayLowerBoundMaxFeelsLikeTemp": 16.54, + "nightLowerBoundMinFeelsLikeTemp": 7.46, + "dayProbabilityOfPrecipitation": 5, + "nightProbabilityOfPrecipitation": 9, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 5, + "nightProbabilityOfRain": 9, + "dayProbabilityOfHeavyRain": 0, + "nightProbabilityOfHeavyRain": 4, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 1, + "dayProbabilityOfSferics": 0, + "nightProbabilityOfSferics": 1 + }, + { + "time": "2022-09-23T00:00Z", + "midday10MWindSpeed": 2.42, + "midnight10MWindSpeed": 1.73, + "midday10MWindDirection": 209, + "midnight10MWindDirection": 306, + "midday10MWindGust": 5.59, + "midnight10MWindGust": 3.31, + "middayVisibility": 30194, + "midnightVisibility": 9598, + "middayRelativeHumidity": 63.53, + "midnightRelativeHumidity": 92.99, + "middayMslp": 101628, + "midnightMslp": 101583, + "maxUvIndex": 3, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 12, + "dayMaxScreenTemperature": 18.76, + "nightMinScreenTemperature": 11.6, + "dayUpperBoundMaxTemp": 21.29, + "nightUpperBoundMinTemp": 13.67, + "dayLowerBoundMaxTemp": 14.51, + "nightLowerBoundMinTemp": 6.44, + "dayMaxFeelsLikeTemp": 17.49, + "nightMinFeelsLikeTemp": 11.38, + "dayUpperBoundMaxFeelsLikeTemp": 20.03, + "nightUpperBoundMinFeelsLikeTemp": 13.12, + "dayLowerBoundMaxFeelsLikeTemp": 14.39, + "nightLowerBoundMinFeelsLikeTemp": 5.45, + "dayProbabilityOfPrecipitation": 43, + "nightProbabilityOfPrecipitation": 45, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 43, + "nightProbabilityOfRain": 45, + "dayProbabilityOfHeavyRain": 22, + "nightProbabilityOfHeavyRain": 26, + "dayProbabilityOfHail": 1, + "nightProbabilityOfHail": 1, + "dayProbabilityOfSferics": 2, + "nightProbabilityOfSferics": 2 + }, + { + "time": "2022-09-24T00:00Z", + "midday10MWindSpeed": 3.95, + "midnight10MWindSpeed": 4.28, + "midday10MWindDirection": 43, + "midnight10MWindDirection": 6, + "midday10MWindGust": 8.46, + "midnight10MWindGust": 8.19, + "middayVisibility": 17042, + "midnightVisibility": 26745, + "middayRelativeHumidity": 74.83, + "midnightRelativeHumidity": 81.26, + "middayMslp": 101763, + "midnightMslp": 101959, + "maxUvIndex": 3, + "daySignificantWeatherCode": 7, + "nightSignificantWeatherCode": 0, + "dayMaxScreenTemperature": 17.4, + "nightMinScreenTemperature": 11.09, + "dayUpperBoundMaxTemp": 22.92, + "nightUpperBoundMinTemp": 13.81, + "dayLowerBoundMaxTemp": 13.72, + "nightLowerBoundMinTemp": 4.24, + "dayMaxFeelsLikeTemp": 15.15, + "nightMinFeelsLikeTemp": 9.74, + "dayUpperBoundMaxFeelsLikeTemp": 21.94, + "nightUpperBoundMinFeelsLikeTemp": 13.29, + "dayLowerBoundMaxFeelsLikeTemp": 11.64, + "nightLowerBoundMinFeelsLikeTemp": 3.44, + "dayProbabilityOfPrecipitation": 32, + "nightProbabilityOfPrecipitation": 10, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 32, + "nightProbabilityOfRain": 10, + "dayProbabilityOfHeavyRain": 17, + "nightProbabilityOfHeavyRain": 6, + "dayProbabilityOfHail": 1, + "nightProbabilityOfHail": 0, + "dayProbabilityOfSferics": 4, + "nightProbabilityOfSferics": 1 + }, + { + "time": "2022-09-25T00:00Z", + "midday10MWindSpeed": 4.36, + "midnight10MWindSpeed": 2.77, + "midday10MWindDirection": 32, + "midnight10MWindDirection": 269, + "midday10MWindGust": 9.45, + "midnight10MWindGust": 4.63, + "middayVisibility": 31004, + "midnightVisibility": 24820, + "middayRelativeHumidity": 60.24, + "midnightRelativeHumidity": 88.87, + "middayMslp": 102022, + "midnightMslp": 101654, + "maxUvIndex": 3, + "daySignificantWeatherCode": 3, + "nightSignificantWeatherCode": 2, + "dayMaxScreenTemperature": 17.65, + "nightMinScreenTemperature": 8.73, + "dayUpperBoundMaxTemp": 22.27, + "nightUpperBoundMinTemp": 13.54, + "dayLowerBoundMaxTemp": 11.66, + "nightLowerBoundMinTemp": 4.02, + "dayMaxFeelsLikeTemp": 15.0, + "nightMinFeelsLikeTemp": 7.98, + "dayUpperBoundMaxFeelsLikeTemp": 21.24, + "nightUpperBoundMinFeelsLikeTemp": 12.32, + "dayLowerBoundMaxFeelsLikeTemp": 11.09, + "nightLowerBoundMinFeelsLikeTemp": 3.6, + "dayProbabilityOfPrecipitation": 9, + "nightProbabilityOfPrecipitation": 6, + "dayProbabilityOfSnow": 0, + "nightProbabilityOfSnow": 0, + "dayProbabilityOfHeavySnow": 0, + "nightProbabilityOfHeavySnow": 0, + "dayProbabilityOfRain": 9, + "nightProbabilityOfRain": 6, + "dayProbabilityOfHeavyRain": 3, + "nightProbabilityOfHeavyRain": 4, + "dayProbabilityOfHail": 0, + "nightProbabilityOfHail": 1, + "dayProbabilityOfSferics": 1, + "nightProbabilityOfSferics": 1 + } + ] + } + } + ], + "parameters": [ + { + "daySignificantWeatherCode": { + "type": "Parameter", + "description": "DaySignificantWeatherCode", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/", + "type": "1" + } + } + }, + "midnightRelativeHumidity": { + "type": "Parameter", + "description": "RelativeHumidityatLocalMidnight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfHeavyRain": { + "type": "Parameter", + "description": "ProbabilityofHeavyRainDuringTheNight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midnight10MWindSpeed": { + "type": "Parameter", + "description": "10mWindSpeedatLocalMidnight", + "unit": { + "label": "metrespersecond", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "nightUpperBoundMinFeelsLikeTemp": { + "type": "Parameter", + "description": "UpperBoundonNightMinimumFeelsLikeAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightUpperBoundMinTemp": { + "type": "Parameter", + "description": "UpperBoundonNightMinimumScreenAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "midnightVisibility": { + "type": "Parameter", + "description": "VisibilityatLocalMidnight", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "dayUpperBoundMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "UpperBoundonDayMaximumFeelsLikeAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfRain": { + "type": "Parameter", + "description": "ProbabilityofRainDuringTheNight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midday10MWindDirection": { + "type": "Parameter", + "description": "10mWindDirectionatLocalMidday", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "nightLowerBoundMinFeelsLikeTemp": { + "type": "Parameter", + "description": "LowerBoundonNightMinimumFeelsLikeAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfHail": { + "type": "Parameter", + "description": "ProbabilityofHailDuringTheNight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "middayMslp": { + "type": "Parameter", + "description": "MeanSeaLevelPressureatLocalMidday", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "dayProbabilityOfHeavySnow": { + "type": "Parameter", + "description": "ProbabilityofHeavySnowDuringTheDay", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfPrecipitation": { + "type": "Parameter", + "description": "ProbabilityofPrecipitationDuringTheNight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfHail": { + "type": "Parameter", + "description": "ProbabilityofHailDuringTheDay", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfRain": { + "type": "Parameter", + "description": "ProbabilityofRainDuringTheDay", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "midday10MWindSpeed": { + "type": "Parameter", + "description": "10mWindSpeedatLocalMidday", + "unit": { + "label": "metrespersecond", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "midday10MWindGust": { + "type": "Parameter", + "description": "10mWindGustSpeedatLocalMidday", + "unit": { + "label": "metrespersecond", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "middayVisibility": { + "type": "Parameter", + "description": "VisibilityatLocalMidday", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "midnight10MWindGust": { + "type": "Parameter", + "description": "10mWindGustSpeedatLocalMidnight", + "unit": { + "label": "metrespersecond", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "midnightMslp": { + "type": "Parameter", + "description": "MeanSeaLevelPressureatLocalMidnight", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "dayProbabilityOfSferics": { + "type": "Parameter", + "description": "ProbabilityofSfericsDuringTheDay", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightSignificantWeatherCode": { + "type": "Parameter", + "description": "NightSignificantWeatherCode", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/", + "type": "1" + } + } + }, + "dayProbabilityOfPrecipitation": { + "type": "Parameter", + "description": "ProbabilityofPrecipitationDuringTheDay", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayProbabilityOfHeavyRain": { + "type": "Parameter", + "description": "ProbabilityofHeavyRainDuringTheDay", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayMaxScreenTemperature": { + "type": "Parameter", + "description": "DayMaximumScreenAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightMinScreenTemperature": { + "type": "Parameter", + "description": "NightMinimumScreenAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "midnight10MWindDirection": { + "type": "Parameter", + "description": "10mWindDirectionatLocalMidnight", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "maxUvIndex": { + "type": "Parameter", + "description": "DayMaximumUVIndex", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + }, + "dayProbabilityOfSnow": { + "type": "Parameter", + "description": "ProbabilityofSnowDuringTheDay", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightProbabilityOfSnow": { + "type": "Parameter", + "description": "ProbabilityofSnowDuringTheNight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayLowerBoundMaxTemp": { + "type": "Parameter", + "description": "LowerBoundonDayMaximumScreenAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfHeavySnow": { + "type": "Parameter", + "description": "ProbabilityofHeavySnowDuringTheNight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "dayLowerBoundMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "LowerBoundonDayMaximumFeelsLikeAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "dayUpperBoundMaxTemp": { + "type": "Parameter", + "description": "UpperBoundonDayMaximumScreenAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "dayMaxFeelsLikeTemp": { + "type": "Parameter", + "description": "DayMaximumFeelsLikeAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "middayRelativeHumidity": { + "type": "Parameter", + "description": "RelativeHumidityatLocalMidday", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "nightLowerBoundMinTemp": { + "type": "Parameter", + "description": "LowerBoundonNightMinimumScreenAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightMinFeelsLikeTemp": { + "type": "Parameter", + "description": "NightMinimumFeelsLikeAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "nightProbabilityOfSferics": { + "type": "Parameter", + "description": "ProbabilityofSfericsDuringTheNight", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + } + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/2022-09-siteHourlyResponse.json b/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/2022-09-siteHourlyResponse.json new file mode 100644 index 0000000000000..97f35c2313ac2 --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/2022-09-siteHourlyResponse.json @@ -0,0 +1,1241 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Point", + "coordinates": [ + -0.32430000000000003, + 51.0624, + 50.0 + ] + }, + "properties": { + "location": { + "name": "Horsham" + }, + "requestPointDistance": 0.1508, + "modelRunDate": "2022-09-17T20:00Z", + "timeSeries": [ + { + "time": "2022-09-17T20:00Z", + "screenTemperature": 8.55, + "maxScreenAirTemp": 10.36, + "minScreenAirTemp": 8.54, + "screenDewPointTemperature": 4.67, + "feelsLikeTemperature": 8.18, + "windSpeed10m": 0.46, + "windDirectionFrom10m": 297, + "windGustSpeed10m": 4.63, + "max10mWindGust": 6.43, + "visibility": 19040, + "screenRelativeHumidity": 76.51, + "mslp": 102230, + "uvIndex": 1, + "significantWeatherCode": 2, + "precipitationRate": 3, + "totalPrecipAmount": 4, + "totalSnowAmount": 5, + "probOfPrecipitation": 60 + }, + { + "time": "2022-09-17T21:00Z", + "screenTemperature": 10.09, + "maxScreenAirTemp": 10.09, + "minScreenAirTemp": 8.55, + "screenDewPointTemperature": 4.6, + "feelsLikeTemperature": 9.77, + "windSpeed10m": 1.3, + "windDirectionFrom10m": 336, + "windGustSpeed10m": 7.55, + "max10mWindGust": 7.65, + "visibility": 20405, + "screenRelativeHumidity": 68.6, + "mslp": 102283, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-17T22:00Z", + "screenTemperature": 10.18, + "maxScreenAirTemp": 10.18, + "minScreenAirTemp": 10.09, + "screenDewPointTemperature": 4.81, + "feelsLikeTemperature": 9.93, + "windSpeed10m": 1.14, + "windDirectionFrom10m": 318, + "windGustSpeed10m": 6.53, + "max10mWindGust": 6.87, + "visibility": 18826, + "screenRelativeHumidity": 69.1, + "mslp": 102260, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-17T23:00Z", + "screenTemperature": 10.05, + "maxScreenAirTemp": 10.18, + "minScreenAirTemp": 10.01, + "screenDewPointTemperature": 4.69, + "feelsLikeTemperature": 9.35, + "windSpeed10m": 1.86, + "windDirectionFrom10m": 309, + "windGustSpeed10m": 7.16, + "max10mWindGust": 7.33, + "visibility": 18786, + "screenRelativeHumidity": 69.11, + "mslp": 102269, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T00:00Z", + "screenTemperature": 9.52, + "maxScreenAirTemp": 10.05, + "minScreenAirTemp": 9.49, + "screenDewPointTemperature": 4.89, + "feelsLikeTemperature": 8.81, + "windSpeed10m": 1.8, + "windDirectionFrom10m": 306, + "windGustSpeed10m": 6.41, + "max10mWindGust": 7.28, + "visibility": 18868, + "screenRelativeHumidity": 72.56, + "mslp": 102250, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T01:00Z", + "screenTemperature": 9.23, + "maxScreenAirTemp": 9.52, + "minScreenAirTemp": 9.19, + "screenDewPointTemperature": 5.01, + "feelsLikeTemperature": 8.19, + "windSpeed10m": 2.12, + "windDirectionFrom10m": 312, + "windGustSpeed10m": 7.34, + "max10mWindGust": 7.64, + "visibility": 19284, + "screenRelativeHumidity": 74.68, + "mslp": 102250, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T02:00Z", + "screenTemperature": 8.9, + "maxScreenAirTemp": 9.23, + "minScreenAirTemp": 8.88, + "screenDewPointTemperature": 5.08, + "feelsLikeTemperature": 7.67, + "windSpeed10m": 2.33, + "windDirectionFrom10m": 307, + "windGustSpeed10m": 7.6, + "max10mWindGust": 8.06, + "visibility": 19338, + "screenRelativeHumidity": 76.68, + "mslp": 102230, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T03:00Z", + "screenTemperature": 8.47, + "maxScreenAirTemp": 8.9, + "minScreenAirTemp": 8.45, + "screenDewPointTemperature": 5.35, + "feelsLikeTemperature": 7.21, + "windSpeed10m": 2.24, + "windDirectionFrom10m": 303, + "windGustSpeed10m": 7.79, + "max10mWindGust": 8.33, + "visibility": 18227, + "screenRelativeHumidity": 80.5, + "mslp": 102192, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T04:00Z", + "screenTemperature": 8.31, + "maxScreenAirTemp": 8.47, + "minScreenAirTemp": 8.29, + "screenDewPointTemperature": 5.52, + "feelsLikeTemperature": 6.88, + "windSpeed10m": 2.42, + "windDirectionFrom10m": 302, + "windGustSpeed10m": 8.17, + "max10mWindGust": 8.65, + "visibility": 18424, + "screenRelativeHumidity": 82.46, + "mslp": 102180, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T05:00Z", + "screenTemperature": 7.96, + "maxScreenAirTemp": 8.31, + "minScreenAirTemp": 7.95, + "screenDewPointTemperature": 5.54, + "feelsLikeTemperature": 6.5, + "windSpeed10m": 2.4, + "windDirectionFrom10m": 294, + "windGustSpeed10m": 7.48, + "max10mWindGust": 8.31, + "visibility": 18185, + "screenRelativeHumidity": 84.47, + "mslp": 102180, + "uvIndex": 0, + "significantWeatherCode": 0, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T06:00Z", + "screenTemperature": 7.87, + "maxScreenAirTemp": 7.96, + "minScreenAirTemp": 7.8, + "screenDewPointTemperature": 5.62, + "feelsLikeTemperature": 6.32, + "windSpeed10m": 2.46, + "windDirectionFrom10m": 297, + "windGustSpeed10m": 7.7, + "max10mWindGust": 8.38, + "visibility": 17945, + "screenRelativeHumidity": 85.7, + "mslp": 102202, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T07:00Z", + "screenTemperature": 9.16, + "maxScreenAirTemp": 9.17, + "minScreenAirTemp": 7.87, + "screenDewPointTemperature": 6.1, + "feelsLikeTemperature": 7.44, + "windSpeed10m": 3.06, + "windDirectionFrom10m": 302, + "windGustSpeed10m": 8.12, + "max10mWindGust": 8.82, + "visibility": 19171, + "screenRelativeHumidity": 81.36, + "mslp": 102214, + "uvIndex": 1, + "significantWeatherCode": 1, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T08:00Z", + "screenTemperature": 10.87, + "maxScreenAirTemp": 10.88, + "minScreenAirTemp": 9.16, + "screenDewPointTemperature": 6.56, + "feelsLikeTemperature": 9.2, + "windSpeed10m": 3.45, + "windDirectionFrom10m": 298, + "windGustSpeed10m": 6.33, + "max10mWindGust": 8.01, + "visibility": 22152, + "screenRelativeHumidity": 74.86, + "mslp": 102215, + "uvIndex": 2, + "significantWeatherCode": 1, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T09:00Z", + "screenTemperature": 12.85, + "maxScreenAirTemp": 12.87, + "minScreenAirTemp": 10.87, + "screenDewPointTemperature": 6.89, + "feelsLikeTemperature": 11.14, + "windSpeed10m": 3.85, + "windDirectionFrom10m": 297, + "windGustSpeed10m": 6.6, + "max10mWindGust": 6.6, + "visibility": 25612, + "screenRelativeHumidity": 67.1, + "mslp": 102205, + "uvIndex": 2, + "significantWeatherCode": 1, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T10:00Z", + "screenTemperature": 14.25, + "maxScreenAirTemp": 14.25, + "minScreenAirTemp": 12.85, + "screenDewPointTemperature": 6.55, + "feelsLikeTemperature": 12.04, + "windSpeed10m": 4.84, + "windDirectionFrom10m": 310, + "windGustSpeed10m": 8.48, + "max10mWindGust": 8.48, + "visibility": 30820, + "screenRelativeHumidity": 60.11, + "mslp": 102205, + "uvIndex": 3, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T11:00Z", + "screenTemperature": 15.21, + "maxScreenAirTemp": 15.22, + "minScreenAirTemp": 14.25, + "screenDewPointTemperature": 6.41, + "feelsLikeTemperature": 12.94, + "windSpeed10m": 4.84, + "windDirectionFrom10m": 315, + "windGustSpeed10m": 8.58, + "max10mWindGust": 8.58, + "visibility": 30123, + "screenRelativeHumidity": 55.92, + "mslp": 102186, + "uvIndex": 3, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 0 + }, + { + "time": "2022-09-18T12:00Z", + "screenTemperature": 16.17, + "maxScreenAirTemp": 16.19, + "minScreenAirTemp": 15.21, + "screenDewPointTemperature": 5.87, + "feelsLikeTemperature": 13.74, + "windSpeed10m": 4.94, + "windDirectionFrom10m": 319, + "windGustSpeed10m": 8.77, + "max10mWindGust": 8.77, + "visibility": 31664, + "screenRelativeHumidity": 50.49, + "mslp": 102175, + "uvIndex": 4, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-18T13:00Z", + "screenTemperature": 16.85, + "maxScreenAirTemp": 16.86, + "minScreenAirTemp": 16.17, + "screenDewPointTemperature": 5.38, + "feelsLikeTemperature": 14.38, + "windSpeed10m": 4.71, + "windDirectionFrom10m": 321, + "windGustSpeed10m": 8.46, + "max10mWindGust": 8.46, + "visibility": 33113, + "screenRelativeHumidity": 46.86, + "mslp": 102155, + "uvIndex": 3, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-18T14:00Z", + "screenTemperature": 17.04, + "maxScreenAirTemp": 17.11, + "minScreenAirTemp": 16.85, + "screenDewPointTemperature": 5.42, + "feelsLikeTemperature": 14.55, + "windSpeed10m": 4.7, + "windDirectionFrom10m": 322, + "windGustSpeed10m": 8.45, + "max10mWindGust": 8.45, + "visibility": 34423, + "screenRelativeHumidity": 46.39, + "mslp": 102134, + "uvIndex": 2, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-18T15:00Z", + "screenTemperature": 17.01, + "maxScreenAirTemp": 17.09, + "minScreenAirTemp": 16.98, + "screenDewPointTemperature": 5.55, + "feelsLikeTemperature": 14.74, + "windSpeed10m": 4.35, + "windDirectionFrom10m": 327, + "windGustSpeed10m": 7.96, + "max10mWindGust": 7.96, + "visibility": 33040, + "screenRelativeHumidity": 46.86, + "mslp": 102115, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-18T16:00Z", + "screenTemperature": 16.66, + "maxScreenAirTemp": 17.01, + "minScreenAirTemp": 16.62, + "screenDewPointTemperature": 5.97, + "feelsLikeTemperature": 14.58, + "windSpeed10m": 4.1, + "windDirectionFrom10m": 333, + "windGustSpeed10m": 7.59, + "max10mWindGust": 7.73, + "visibility": 31599, + "screenRelativeHumidity": 49.45, + "mslp": 102125, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-18T17:00Z", + "screenTemperature": 15.95, + "maxScreenAirTemp": 16.66, + "minScreenAirTemp": 15.93, + "screenDewPointTemperature": 6.7, + "feelsLikeTemperature": 14.31, + "windSpeed10m": 3.51, + "windDirectionFrom10m": 338, + "windGustSpeed10m": 6.76, + "max10mWindGust": 7.83, + "visibility": 29182, + "screenRelativeHumidity": 54.37, + "mslp": 102145, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-18T18:00Z", + "screenTemperature": 15.15, + "maxScreenAirTemp": 15.95, + "minScreenAirTemp": 15.14, + "screenDewPointTemperature": 7.3, + "feelsLikeTemperature": 14.12, + "windSpeed10m": 2.58, + "windDirectionFrom10m": 334, + "windGustSpeed10m": 5.79, + "max10mWindGust": 6.81, + "visibility": 26430, + "screenRelativeHumidity": 59.57, + "mslp": 102166, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-18T19:00Z", + "screenTemperature": 14.5, + "maxScreenAirTemp": 15.15, + "minScreenAirTemp": 14.48, + "screenDewPointTemperature": 7.6, + "feelsLikeTemperature": 13.52, + "windSpeed10m": 2.56, + "windDirectionFrom10m": 336, + "windGustSpeed10m": 6.1, + "max10mWindGust": 6.66, + "visibility": 24900, + "screenRelativeHumidity": 63.38, + "mslp": 102216, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-18T20:00Z", + "screenTemperature": 13.86, + "maxScreenAirTemp": 14.5, + "minScreenAirTemp": 13.83, + "screenDewPointTemperature": 7.93, + "feelsLikeTemperature": 12.99, + "windSpeed10m": 2.39, + "windDirectionFrom10m": 334, + "windGustSpeed10m": 6.05, + "max10mWindGust": 6.63, + "visibility": 23605, + "screenRelativeHumidity": 67.63, + "mslp": 102257, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-18T21:00Z", + "screenTemperature": 13.38, + "maxScreenAirTemp": 13.86, + "minScreenAirTemp": 13.35, + "screenDewPointTemperature": 8.24, + "feelsLikeTemperature": 12.52, + "windSpeed10m": 2.42, + "windDirectionFrom10m": 341, + "windGustSpeed10m": 6.28, + "max10mWindGust": 6.82, + "visibility": 23266, + "screenRelativeHumidity": 71.15, + "mslp": 102288, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-18T22:00Z", + "screenTemperature": 12.68, + "maxScreenAirTemp": 13.38, + "minScreenAirTemp": 12.65, + "screenDewPointTemperature": 8.4, + "feelsLikeTemperature": 11.86, + "windSpeed10m": 2.32, + "windDirectionFrom10m": 340, + "windGustSpeed10m": 6.03, + "max10mWindGust": 7.12, + "visibility": 22500, + "screenRelativeHumidity": 75.51, + "mslp": 102308, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-18T23:00Z", + "screenTemperature": 12.1, + "maxScreenAirTemp": 12.68, + "minScreenAirTemp": 12.06, + "screenDewPointTemperature": 8.61, + "feelsLikeTemperature": 11.46, + "windSpeed10m": 2.01, + "windDirectionFrom10m": 338, + "windGustSpeed10m": 5.07, + "max10mWindGust": 6.35, + "visibility": 20354, + "screenRelativeHumidity": 79.47, + "mslp": 102336, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T00:00Z", + "screenTemperature": 11.7, + "maxScreenAirTemp": 12.1, + "minScreenAirTemp": 11.61, + "screenDewPointTemperature": 8.7, + "feelsLikeTemperature": 11.07, + "windSpeed10m": 1.94, + "windDirectionFrom10m": 351, + "windGustSpeed10m": 4.7, + "max10mWindGust": 5.32, + "visibility": 20170, + "screenRelativeHumidity": 82.06, + "mslp": 102346, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T01:00Z", + "screenTemperature": 11.18, + "maxScreenAirTemp": 11.7, + "minScreenAirTemp": 11.15, + "screenDewPointTemperature": 8.61, + "feelsLikeTemperature": 10.57, + "windSpeed10m": 1.83, + "windDirectionFrom10m": 337, + "windGustSpeed10m": 4.23, + "max10mWindGust": 5.05, + "visibility": 19729, + "screenRelativeHumidity": 84.51, + "mslp": 102347, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T02:00Z", + "screenTemperature": 10.72, + "maxScreenAirTemp": 11.18, + "minScreenAirTemp": 10.67, + "screenDewPointTemperature": 8.38, + "feelsLikeTemperature": 10.21, + "windSpeed10m": 1.66, + "windDirectionFrom10m": 335, + "windGustSpeed10m": 3.89, + "max10mWindGust": 4.44, + "visibility": 17930, + "screenRelativeHumidity": 85.7, + "mslp": 102347, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T03:00Z", + "screenTemperature": 10.43, + "maxScreenAirTemp": 10.72, + "minScreenAirTemp": 10.26, + "screenDewPointTemperature": 8.45, + "feelsLikeTemperature": 10.05, + "windSpeed10m": 1.35, + "windDirectionFrom10m": 313, + "windGustSpeed10m": 3.14, + "max10mWindGust": 4.07, + "visibility": 16819, + "screenRelativeHumidity": 87.85, + "mslp": 102348, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T04:00Z", + "screenTemperature": 10.18, + "maxScreenAirTemp": 10.43, + "minScreenAirTemp": 10.12, + "screenDewPointTemperature": 8.2, + "feelsLikeTemperature": 9.77, + "windSpeed10m": 1.36, + "windDirectionFrom10m": 328, + "windGustSpeed10m": 2.99, + "max10mWindGust": 3.74, + "visibility": 16296, + "screenRelativeHumidity": 87.89, + "mslp": 102374, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T05:00Z", + "screenTemperature": 9.98, + "maxScreenAirTemp": 10.18, + "minScreenAirTemp": 9.89, + "screenDewPointTemperature": 8.28, + "feelsLikeTemperature": 9.55, + "windSpeed10m": 1.45, + "windDirectionFrom10m": 312, + "windGustSpeed10m": 3.02, + "max10mWindGust": 3.92, + "visibility": 13673, + "screenRelativeHumidity": 89.56, + "mslp": 102404, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 2 + }, + { + "time": "2022-09-19T06:00Z", + "screenTemperature": 9.79, + "maxScreenAirTemp": 10.08, + "minScreenAirTemp": 9.58, + "screenDewPointTemperature": 8.33, + "feelsLikeTemperature": 9.37, + "windSpeed10m": 1.33, + "windDirectionFrom10m": 326, + "windGustSpeed10m": 3.32, + "max10mWindGust": 3.79, + "visibility": 12471, + "screenRelativeHumidity": 91.04, + "mslp": 102436, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T07:00Z", + "screenTemperature": 10.9, + "maxScreenAirTemp": 10.91, + "minScreenAirTemp": 9.79, + "screenDewPointTemperature": 8.79, + "feelsLikeTemperature": 10.53, + "windSpeed10m": 1.45, + "windDirectionFrom10m": 327, + "windGustSpeed10m": 3.43, + "max10mWindGust": 4.07, + "visibility": 12829, + "screenRelativeHumidity": 87.3, + "mslp": 102466, + "uvIndex": 1, + "significantWeatherCode": 3, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T08:00Z", + "screenTemperature": 12.01, + "maxScreenAirTemp": 12.01, + "minScreenAirTemp": 10.9, + "screenDewPointTemperature": 9.21, + "feelsLikeTemperature": 11.72, + "windSpeed10m": 1.37, + "windDirectionFrom10m": 275, + "windGustSpeed10m": 2.93, + "max10mWindGust": 3.5, + "visibility": 17147, + "screenRelativeHumidity": 83.49, + "mslp": 102486, + "uvIndex": 2, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2022-09-19T09:00Z", + "screenTemperature": 13.35, + "maxScreenAirTemp": 13.35, + "minScreenAirTemp": 12.01, + "screenDewPointTemperature": 9.36, + "feelsLikeTemperature": 13.07, + "windSpeed10m": 1.47, + "windDirectionFrom10m": 311, + "windGustSpeed10m": 2.94, + "max10mWindGust": 2.94, + "visibility": 20347, + "screenRelativeHumidity": 77.29, + "mslp": 102497, + "uvIndex": 2, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-19T10:00Z", + "screenTemperature": 14.36, + "maxScreenAirTemp": 14.37, + "minScreenAirTemp": 13.35, + "screenDewPointTemperature": 9.14, + "feelsLikeTemperature": 13.87, + "windSpeed10m": 1.85, + "windDirectionFrom10m": 296, + "windGustSpeed10m": 3.71, + "max10mWindGust": 3.71, + "visibility": 23360, + "screenRelativeHumidity": 71.6, + "mslp": 102515, + "uvIndex": 2, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-19T11:00Z", + "screenTemperature": 15.28, + "maxScreenAirTemp": 15.31, + "minScreenAirTemp": 14.36, + "screenDewPointTemperature": 8.96, + "feelsLikeTemperature": 14.54, + "windSpeed10m": 2.29, + "windDirectionFrom10m": 295, + "windGustSpeed10m": 4.55, + "max10mWindGust": 4.55, + "visibility": 25109, + "screenRelativeHumidity": 66.86, + "mslp": 102505, + "uvIndex": 3, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2022-09-19T12:00Z", + "screenTemperature": 16.01, + "maxScreenAirTemp": 16.03, + "minScreenAirTemp": 15.28, + "screenDewPointTemperature": 8.73, + "feelsLikeTemperature": 15.04, + "windSpeed10m": 2.63, + "windDirectionFrom10m": 297, + "windGustSpeed10m": 5.3, + "max10mWindGust": 5.3, + "visibility": 26780, + "screenRelativeHumidity": 62.73, + "mslp": 102494, + "uvIndex": 3, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2022-09-19T13:00Z", + "screenTemperature": 16.37, + "maxScreenAirTemp": 16.4, + "minScreenAirTemp": 16.01, + "screenDewPointTemperature": 8.74, + "feelsLikeTemperature": 15.37, + "windSpeed10m": 2.66, + "windDirectionFrom10m": 304, + "windGustSpeed10m": 5.43, + "max10mWindGust": 5.43, + "visibility": 26670, + "screenRelativeHumidity": 61.18, + "mslp": 102473, + "uvIndex": 3, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2022-09-19T14:00Z", + "screenTemperature": 16.68, + "maxScreenAirTemp": 16.97, + "minScreenAirTemp": 16.37, + "screenDewPointTemperature": 8.62, + "feelsLikeTemperature": 15.53, + "windSpeed10m": 2.85, + "windDirectionFrom10m": 306, + "windGustSpeed10m": 5.68, + "max10mWindGust": 5.68, + "visibility": 28030, + "screenRelativeHumidity": 59.53, + "mslp": 102452, + "uvIndex": 2, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2022-09-19T15:00Z", + "screenTemperature": 16.93, + "maxScreenAirTemp": 17.02, + "minScreenAirTemp": 16.68, + "screenDewPointTemperature": 8.63, + "feelsLikeTemperature": 15.72, + "windSpeed10m": 2.96, + "windDirectionFrom10m": 315, + "windGustSpeed10m": 5.76, + "max10mWindGust": 5.76, + "visibility": 28413, + "screenRelativeHumidity": 58.57, + "mslp": 102416, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-19T16:00Z", + "screenTemperature": 16.11, + "maxScreenAirTemp": 16.79, + "minScreenAirTemp": 15.97, + "screenDewPointTemperature": 9.0, + "feelsLikeTemperature": 15.35, + "windSpeed10m": 2.28, + "windDirectionFrom10m": 321, + "windGustSpeed10m": 4.56, + "max10mWindGust": 4.56, + "visibility": 27394, + "screenRelativeHumidity": 63.61, + "mslp": 102400, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 5 + }, + { + "time": "2022-09-19T17:00Z", + "screenTemperature": 15.32, + "maxScreenAirTemp": 16.11, + "minScreenAirTemp": 15.32, + "screenDewPointTemperature": 9.35, + "feelsLikeTemperature": 14.76, + "windSpeed10m": 2.02, + "windDirectionFrom10m": 314, + "windGustSpeed10m": 3.82, + "max10mWindGust": 3.82, + "visibility": 25612, + "screenRelativeHumidity": 68.54, + "mslp": 102398, + "uvIndex": 1, + "significantWeatherCode": 7, + "precipitationRate": 0, + "totalPrecipAmount": 0, + "totalSnowAmount": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-19T18:00Z", + "screenTemperature": 14.38, + "screenDewPointTemperature": 10.03, + "feelsLikeTemperature": 14.09, + "windSpeed10m": 1.7, + "windDirectionFrom10m": 310, + "windGustSpeed10m": 3.05, + "visibility": 24267, + "screenRelativeHumidity": 75.61, + "mslp": 102388, + "uvIndex": 0, + "significantWeatherCode": 7, + "precipitationRate": 0, + "probOfPrecipitation": 4 + }, + { + "time": "2022-09-19T19:00Z", + "screenTemperature": 13.51, + "screenDewPointTemperature": 9.89, + "feelsLikeTemperature": 13.18, + "windSpeed10m": 1.75, + "windDirectionFrom10m": 302, + "windGustSpeed10m": 3.09, + "visibility": 24262, + "screenRelativeHumidity": 79.35, + "mslp": 102415, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "probOfPrecipitation": 1 + }, + { + "time": "2022-09-19T20:00Z", + "screenTemperature": 12.91, + "screenDewPointTemperature": 9.79, + "feelsLikeTemperature": 12.56, + "windSpeed10m": 1.7, + "windDirectionFrom10m": 295, + "windGustSpeed10m": 3.04, + "visibility": 21268, + "screenRelativeHumidity": 82.04, + "mslp": 102435, + "uvIndex": 0, + "significantWeatherCode": 2, + "precipitationRate": 0, + "probOfPrecipitation": 1 + } + ] + } + } + ], + "parameters": [ + { + "totalSnowAmount": { + "type": "Parameter", + "description": "TotalSnowAmountOverPreviousHour", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "screenTemperature": { + "type": "Parameter", + "description": "ScreenAirTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "visibility": { + "type": "Parameter", + "description": "Visibility", + "unit": { + "label": "metres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m" + } + } + }, + "windDirectionFrom10m": { + "type": "Parameter", + "description": "10mWindFromDirection", + "unit": { + "label": "degrees", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "deg" + } + } + }, + "precipitationRate": { + "type": "Parameter", + "description": "PrecipitationRate", + "unit": { + "label": "millimetresperhour", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm/h" + } + } + }, + "maxScreenAirTemp": { + "type": "Parameter", + "description": "MaximumScreenAirTemperatureOverPreviousHour", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "feelsLikeTemperature": { + "type": "Parameter", + "description": "FeelsLikeTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "screenDewPointTemperature": { + "type": "Parameter", + "description": "ScreenDewPointTemperature", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "screenRelativeHumidity": { + "type": "Parameter", + "description": "ScreenRelativeHumidity", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "windSpeed10m": { + "type": "Parameter", + "description": "10mWindSpeed", + "unit": { + "label": "metrespersecond", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "probOfPrecipitation": { + "type": "Parameter", + "description": "ProbabilityofPrecipitation", + "unit": { + "label": "percentage", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "%" + } + } + }, + "max10mWindGust": { + "type": "Parameter", + "description": "Maximum10mWindGustSpeedOverPreviousHour", + "unit": { + "label": "metrespersecond", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "significantWeatherCode": { + "type": "Parameter", + "description": "SignificantWeatherCode", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "https://metoffice.apiconnect.ibmcloud.com/metoffice/production/", + "type": "1" + } + } + }, + "minScreenAirTemp": { + "type": "Parameter", + "description": "MinimumScreenAirTemperatureOverPreviousHour", + "unit": { + "label": "degreesCelsius", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Cel" + } + } + }, + "totalPrecipAmount": { + "type": "Parameter", + "description": "TotalPrecipitationAmountOverPreviousHour", + "unit": { + "label": "millimetres", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "mm" + } + } + }, + "pressure": { + "type": "Parameter", + "description": "MeanSeaLevelPressure", + "unit": { + "label": "pascals", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "Pa" + } + } + }, + "windGustSpeed10m": { + "type": "Parameter", + "description": "10mWindGustSpeed", + "unit": { + "label": "metrespersecond", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "m/s" + } + } + }, + "uvIndex": { + "type": "Parameter", + "description": "UVIndex", + "unit": { + "label": "dimensionless", + "symbol": { + "value": "http://www.opengis.net/def/uom/UCUM/", + "type": "1" + } + } + } + } + ] +} \ No newline at end of file diff --git a/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureCollectionTest.java b/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureCollectionTest.java new file mode 100644 index 0000000000000..925005167300e --- /dev/null +++ b/bundles/org.openhab.binding.metofficedatahub/src/main/test/org/openhab/binding/metofficedatahub/internal/dto/responses/SiteApiFeatureCollectionTest.java @@ -0,0 +1,238 @@ +/** + * Copyright (c) 2010-2024 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.metofficedatahub.internal.dto.responses; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.openhab.binding.metofficedatahub.internal.MetOfficeDataHubBindingConstants; + +/** + * The {@link SiteApiFeatureCollectionTest} class implements unit test case for {@link SiteApiFeatureCollection} + * + * @author David Goodyear - Initial contribution + */ +@NonNullByDefault +public class SiteApiFeatureCollectionTest { + + private @Nullable String siteApiDailyResponse = null; + private @Nullable String siteApiHourlyResponse = null; + + public @Nullable String getSiteDailyApiResponse() { + try { + if (siteApiDailyResponse == null) { + java.net.URL url = SiteApiFeatureCollectionTest.class.getResource("2022-09-siteDailyResponse.json"); + if (url == null) { + return null; + } + java.nio.file.Path resPath = java.nio.file.Paths.get(url.toURI()); + siteApiDailyResponse = new String(java.nio.file.Files.readAllBytes(resPath), "UTF8"); + } + } catch (Exception e) { + return null; + } + return siteApiDailyResponse; + } + + public @Nullable String getSiteHourlyApiResponse() { + try { + if (siteApiHourlyResponse == null) { + java.net.URL url = SiteApiFeatureCollectionTest.class.getResource("2022-09-siteHourlyResponse.json"); + if (url == null) { + return null; + } + java.nio.file.Path resPath = java.nio.file.Paths.get(url.toURI()); + siteApiHourlyResponse = new String(java.nio.file.Files.readAllBytes(resPath), "UTF8"); + } + + } catch (Exception e) { + return null; + } + return siteApiHourlyResponse; + } + + @Test + public void testSiteApiFeatureCollectionHourly() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteHourlyApiResponse(), + SiteApiFeatureCollection.class); + if (response != null) { + assertEquals(SiteApiFeatureCollection.TYPE_SITE_API_FEATURE_COLLECTION, response.getType()); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testSiteApiFeatureCollectionDaily() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteDailyApiResponse(), + SiteApiFeatureCollection.class); + if (response != null) { + assertEquals(SiteApiFeatureCollection.TYPE_SITE_API_FEATURE_COLLECTION, response.getType()); + } else { + fail("GSON returned null"); + } + } + + @Test + public void testSiteApiFeatureHourly() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteHourlyApiResponse(), + SiteApiFeatureCollection.class); + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1, response.getFeature().length); + assertEquals(SiteApiFeature.TYPE_SITE_API_FEATURE, response.getFeature()[0].getType()); + } + + @Test + public void testSiteApiFeatureDaily() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteDailyApiResponse(), + SiteApiFeatureCollection.class); + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1,response.getFeature().length); + assertEquals(SiteApiFeature.TYPE_SITE_API_FEATURE, response.getFeature()[0].getType()); + } + + @Test + public void testSiteApiFeatureGeometryHourly() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteHourlyApiResponse(), + SiteApiFeatureCollection.class); + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1, response.getFeature().length); + assertEquals(SiteApiFeaturePoint.TYPE_SITE_API_FEATURE, response.getFeature()[0].getGeometry().getType()); + assertEquals(-0.32430000000000003,response.getFeature()[0].getGeometry().getLongitude()); + assertEquals(51.0624,response.getFeature()[0].getGeometry().getLatitude()); + assertEquals(50.0,response.getFeature()[0].getGeometry().getElevation()); + } + + @Test + public void testSiteApiFeatureGeometryDaily() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteDailyApiResponse(), + SiteApiFeatureCollection.class); + + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1, response.getFeature().length); + assertEquals(SiteApiFeaturePoint.TYPE_SITE_API_FEATURE, response.getFeature()[0].getGeometry().getType()); + assertEquals(-0.32430000000000003,response.getFeature()[0].getGeometry().getLongitude()); + assertEquals(51.0624,response.getFeature()[0].getGeometry().getLatitude()); + assertEquals(50.0,response.getFeature()[0].getGeometry().getElevation()); + } + + @Test + public void testSiteApiFeaturePropertiesHourly() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteHourlyApiResponse(), + SiteApiFeatureCollection.class); + + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1, response.getFeature().length); + assertEquals("Horsham",response.getFeature()[0].getProperties().getLocation().getName()); + assertEquals(0.1508,response.getFeature()[0].getProperties().getRequestPointDistance()); + assertEquals("2022-09-17T20:00Z",response.getFeature()[0].getProperties().getModelRunDate()); + } + + @Test + public void testSiteApiFeaturePropertiesDaily() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteDailyApiResponse(), + SiteApiFeatureCollection.class); + + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1, response.getFeature().length); + assertEquals("Horsham",response.getFeature()[0].getProperties().getLocation().getName()); + assertEquals(0.1508,response.getFeature()[0].getProperties().getRequestPointDistance()); + assertEquals("2022-09-19T21:00Z",response.getFeature()[0].getProperties().getModelRunDate()); + } + + @Test + public void testSiteApiFeaturePropertiesTimeSeries0Hourly() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteHourlyApiResponse(), + SiteApiFeatureCollection.class); + + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1, response.getFeature().length); + assertEquals("2022-09-17T20:00Z",response.getFeature()[0].getProperties().getTimeSeries()[0].getTime()); + assertEquals( 8.55,response.getFeature()[0].getProperties().getTimeSeries()[0].getScreenTemperature()); + assertEquals( 10.36,response.getFeature()[0].getProperties().getTimeSeries()[0].getMaxScreenTemperature()); + assertEquals( 8.54,response.getFeature()[0].getProperties().getTimeSeries()[0].getMinScreenTemperature()); + assertEquals( 4.67,response.getFeature()[0].getProperties().getTimeSeries()[0].getScreenDewPointTemperature()); + assertEquals( 8.18,response.getFeature()[0].getProperties().getTimeSeries()[0].getFeelsLikeTemperature()); + assertEquals( 0.46,response.getFeature()[0].getProperties().getTimeSeries()[0].getWindSpeed10m()); + assertEquals( 297,response.getFeature()[0].getProperties().getTimeSeries()[0].getWindDirectionFrom10m()); + assertEquals( 4.63,response.getFeature()[0].getProperties().getTimeSeries()[0].getWindGustSpeed10m()); + assertEquals( 6.43,response.getFeature()[0].getProperties().getTimeSeries()[0].getMax10mWindGust()); + assertEquals( 19040,response.getFeature()[0].getProperties().getTimeSeries()[0].getVisibility()); + assertEquals( 76.51,response.getFeature()[0].getProperties().getTimeSeries()[0].getScreenRelativeHumidity()); + assertEquals( 102230,response.getFeature()[0].getProperties().getTimeSeries()[0].getPressure()); + assertEquals( 1,response.getFeature()[0].getProperties().getTimeSeries()[0].getUvIndex()); + assertEquals( 2,response.getFeature()[0].getProperties().getTimeSeries()[0].getSignificantWeatherCode()); + assertEquals( 3,response.getFeature()[0].getProperties().getTimeSeries()[0].getPrecipitationRate()); + assertEquals( 4,response.getFeature()[0].getProperties().getTimeSeries()[0].getTotalPrecipAmount()); + assertEquals( 5,response.getFeature()[0].getProperties().getTimeSeries()[0].getTotalSnowAmount()); + assertEquals( 60,response.getFeature()[0].getProperties().getTimeSeries()[0].getProbOfPrecipitation()); + } + + @Test + public void testSiteApiFeaturePropertiesTimeSeries0Daily() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteDailyApiResponse(), + SiteApiFeatureCollection.class); + + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1, response.getFeature().length); + assertEquals("2022-09-18T00:00Z",response.getFeature()[0].getProperties().getTimeSeries()[0].getTime()); + assertEquals( 1.39,response.getFeature()[0].getProperties().getTimeSeries()[0].getMidnight10MWindSpeed()); + assertEquals( 31,response.getFeature()[0].getProperties().getTimeSeries()[0].getMidnight10MWindDirection()); + assertEquals( 5.66,response.getFeature()[0].getProperties().getTimeSeries()[0].getMidnight10MWindGust()); + assertEquals( 8776,response.getFeature()[0].getProperties().getTimeSeries()[0].getMidnightVisibility()); + assertEquals( 91.42,response.getFeature()[0].getProperties().getTimeSeries()[0].getMidnightRelativeHumidity()); + assertEquals( 102310,response.getFeature()[0].getProperties().getTimeSeries()[0].getMidnightPressure()); + assertEquals( 2,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightSignificantWeatherCode()); + assertEquals( 8.05,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightMinScreenTemperature()); + assertEquals( 12.79,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightUpperBoundMinTemp()); + assertEquals( 5.51,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightLowerBoundMinTemp()); + assertEquals( 7.35,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightMinFeelsLikeTemp()); + assertEquals( 12.83,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightUpperBoundMinFeelsLikeTemp()); + assertEquals( 7.33,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightLowerBoundMinFeelsLikeTemp()); + assertEquals( 5,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightProbabilityOfPrecipitation()); + assertEquals( 0,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightProbabilityOfSnow()); + assertEquals( 1,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightProbabilityOfHeavySnow()); + assertEquals( 5,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightProbabilityOfRain()); + assertEquals( 3,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightProbabilityOfHeavyRain()); + assertEquals( 4,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightProbabilityOfHail()); + assertEquals( 6,response.getFeature()[0].getProperties().getTimeSeries()[0].getNightProbabilityOfSferics()); + } + + @Test + public void testGetTimeSeriesForCurrentHour() { + SiteApiFeatureCollection response = MetOfficeDataHubBindingConstants.GSON.fromJson(getSiteHourlyApiResponse(), + SiteApiFeatureCollection.class); + + assertNotNull(response); + assertNotNull(response.getFeature()); + assertEquals(1, response.getFeature().length); + assertEquals( 0,response.getFeature()[0].getProperties().getHourlyTimeSeriesPositionForCurrentHour("2022-09-17T20:00Z")); + assertEquals( 1,response.getFeature()[0].getProperties().getHourlyTimeSeriesPositionForCurrentHour("2022-09-17T21:00Z")); + assertEquals( 2,response.getFeature()[0].getProperties().getHourlyTimeSeriesPositionForCurrentHour("2022-09-17T22:00Z")); + assertEquals( 3,response.getFeature()[0].getProperties().getHourlyTimeSeriesPositionForCurrentHour("2022-09-17T23:00Z")); + assertEquals( 24,response.getFeature()[0].getProperties().getHourlyTimeSeriesPositionForCurrentHour("2022-09-18T20:00Z")); + assertEquals( 48,response.getFeature()[0].getProperties().getHourlyTimeSeriesPositionForCurrentHour("2022-09-19T20:00Z")); + } + +} diff --git a/bundles/pom.xml b/bundles/pom.xml index 95995ea9d3217..fe57819ae34e2 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -253,6 +253,7 @@ org.openhab.binding.meteoblue org.openhab.binding.meteofrance org.openhab.binding.meteostick + org.openhab.binding.metofficedatahub org.openhab.binding.mffan org.openhab.binding.miele org.openhab.binding.mielecloud