diff --git a/plugins/weather/example_graphs/wunderground_STATION-week.png b/plugins/weather/example_graphs/wunderground_STATION-week.png new file mode 100644 index 00000000..e978abb2 Binary files /dev/null and b/plugins/weather/example_graphs/wunderground_STATION-week.png differ diff --git a/plugins/weather/wunderground_ b/plugins/weather/wunderground_ new file mode 100755 index 00000000..c81f0d87 --- /dev/null +++ b/plugins/weather/wunderground_ @@ -0,0 +1,361 @@ +#!/bin/sh +# -*- sh -*- + +: << =cut + +=head1 NAME + +wunderground - Plugin to monitor weather stations through Weather Underground + +The precipitation rate is recalculated from the daily cumulative sum +precipTotal, as a DERIVE value, rather than using the immediate precipRate as a +GAUGE. This allows to have more correct aggregates for the +weekly/monstly/yearly graphs, particularly in case of missing values. + +However, to work around the limitation that DERIVE only supports integers, the +decimal point is also shifted two places right (*100) from the reported value, +so even small values can be represented. The value is then rescaled to cancel +this shift (/100), and expressed per hour rather than second (*3600), as it is +the usual unit for precipitation. + +=head1 CONFIGURATION + + [wunderground] + env.api_key 6532d6454b8aa370768e63d6ba5a832e # this is the default; it seems to be what the website uses + env.station_id KCASANFR1708 + env.units metric # optional, this is the default + env.base_url # optional, default to https://api.weather.com/v2/pws/observations/current + env.connect_timeout 1 # optional, amount to wait for requests, in seconds + +Alternatively, the station_id can be encoded in the name of the symlink as +wunderground_STATIONID (e.g., wundergound_KCASANFR1708). This allows to monitor +multiple stations at once. The configuration can then omit the station_id (it +will be ignored if present), and only one section can be used for all instances +of the plugin. + + [wunderground_*] + env.api_key 6532d6454b8aa370768e63d6ba5a832e # this is the default; it seems to be what the website uses + env.units metric # optional, this is the default + env.base_url # optional, default to https://api.weather.com/v2/pws/observations/current + env.connect_timeout 1 # optional, amount to wait for requests, in seconds + +=head1 AUTHOR + +Olivier Mehani + +Copyright (C) 2020 Olivier Mehani + +=head1 LICENSE + +SPDX-License-Identifier: GPL-3.0-or-later + +=head1 MAGIC MARKERS + + #%# family=manual + +=cut + +# Example output +# +# curl 'https://api.weather.com/v2/pws/observations/current?apiKey=6532d6454b8aa370768e63d6ba5a832e&stationId=KCASANFR1708&numericPrecision=decimal&format=json&units=m' +#{"observations":[{"stationID":"KCASANFR1708","obsTimeUtc":"2020-06-15T06:30:54Z","obsTimeLocal":"2020-06-14 23:30:54","neighborhood":"Van Ness - Civic Center","softwareType":"Weather logger V3.0.8","country":"US","solarRadiation":null,"lon":-122.423,"realtimeFrequency":null,"epoch":1592202654,"lat":37.788,"uv":null,"winddir":null,"humidity":90.0,"qcStatus":1,"imperial":{"temp":58.6,"heatIndex":58.6,"dewpt":55.8,"windChill":null,"windSpeed":null,"windGust":null,"pressure":29.85,"precipRate":null,"precipTotal":null,"elev":187.0}}]} + +set -eu + +# shellcheck disable=SC1090 +. "${MUNIN_LIBDIR}/plugins/plugin.sh" + +if [ "${MUNIN_DEBUG:-0}" = 1 ]; then + set -x +fi + +PLUGIN_NAME="$(basename "${0}")" +STATION_ID="$(echo "${PLUGIN_NAME}" | sed 's/.*_//')" +# Use the station ID from the config only if the plugin doesn't specify one +STATION_ID=${STATION_ID:-${station_id:-KCASANFR1708}} + +API_KEY=${api_key:-6532d6454b8aa370768e63d6ba5a832e} +UNITS=${units:-metric} +BASE_URL=${base_url:-https://api.weather.com/v2/pws/observations/current} +CONNECT_TIMEOUT=${connect_timeout:-1} + +UNITS_ARG='&units=m' +DISTANCE_UNIT='m' +PRESSURE_UNIT='hPa' +PRECIPITATION_UNIT='mm' +SPEED_UNIT='km/h' +TEMP_UNIT='°C' +# https://en.wikipedia.org/wiki/Wind_chill#/media/File:Windchill_effect_en.svg +WIND_CHILL_CAUTION=-35: +WIND_CHILL_DANGER=-60: +# https://en.wikipedia.org/wiki/Heat_index#Table_of_values +HEAT_INDEX_CAUTION=27 +HEAT_INDEX_EXTREME_CAUTION=33 +HEAT_INDEX_DANGER=41 +HEAT_INDEX_EXTREME_DANGER=54 +if [ "${UNITS}" = "imperial" ]; then + UNITS_ARG='&units=e' + DISTANCE_UNIT='ft' + PRESSURE_UNIT='in' + PRECIPITATION_UNIT='in' + SPEED_UNIT='mph' + TEMP_UNIT='°F' + WIND_CHILL_CAUTION=-31: + WIND_CHILL_DANGER=-76:w + HEAT_INDEX_CAUTION=80 + HEAT_INDEX_EXTREME_CAUTION=91 + HEAT_INDEX_DANGER=105 + HEAT_INDEX_EXTREME_DANGER=130 +fi +API_URL="${BASE_URL}?apiKey=${API_KEY}&stationId=${STATION_ID}&numericPrecision=decimal&format=json${UNITS_ARG}" + +check_deps() { + for CMD in curl jq; do + if ! command -v "${CMD}" >/dev/null; then + echo "no (${CMD} not found)" + fi + done +} + +CURL_ARGS="-s --connect-timeout ${CONNECT_TIMEOUT}" +fetch() { + # shellcheck disable=SC2086 + curl -f ${CURL_ARGS} "$@" \ + || { echo "error fetching ${*}" >&2; false; } +} + +config() { + local STATION_INFO="in \(.neighborhood), \(.country) reported by station \(.stationID) (\(.lon), \(.lat), \(.${UNITS}.elev) m) at \(.obsTimeLocal) (\(.obsTimeUtc))" + fetch "${API_URL}" | jq -r ".observations[0] + | @text \" +multigraph wunderground_${STATION_ID} +graph_title Weather in \(.neighborhood) +graph_info Weather ${STATION_INFO} +graph_category weather +graph_vlabel Temperature / UV Index / Precipitation +temp.label Temperature [${TEMP_UNIT}] +windChill.label Wind chill [${TEMP_UNIT}] +heatIndex.label Heat index [$TEMP_UNIT] +uv.label UV index +precipRate.draw AREA +precipRate.label Precipitation rate [${PRECIPITATION_UNIT} per hour] + +multigraph wunderground_${STATION_ID}.air_humidity +graph_title Humidity in \(.neighborhood) +graph_info Humidity ${STATION_INFO} +graph_category weather +graph_args -l 0 --upper-limit 100 +graph_vlabel Humidity [%] +humidity.label Humidity +humidity.min 0 +humidity.max 100 + +multigraph wunderground_${STATION_ID}.location +graph_title Location of \(.stationID) +graph_info Track geographic coordinates of station \(.stationID); last: \(.lon), \(.lat), \(.${UNITS}.elev) ${DISTANCE_UNIT} at \(.obsTimeLocal) (\(.obsTimeUtc)) +graph_category weather +graph_scale no +graph_vlabel lon/lat [°] / elevation [${DISTANCE_UNIT}] +lon.label Longitude [°] +lat.label Latitude [°] +elev.label Elevation [${DISTANCE_UNIT}] + +multigraph wunderground_${STATION_ID}.precipitation +graph_title Precipitation in \(.neighborhood) +graph_info Precipitation ${STATION_INFO} +graph_category weather +graph_args -l 0 --base 1000 +graph_vlabel Precipitation [${PRECIPITATION_UNIT} per hour] +precipRate.label Precipitation rate +avgRate.label Average precipitation rate +avgRate.type DERIVE +avgRate.draw AREA +avgRate.min 0 +avgRate.cdef avgRate,36,* + +multigraph wunderground_${STATION_ID}.air_pressure +graph_title Pressure in \(.neighborhood) +graph_info Pressure ${STATION_INFO} +graph_category weather +graph_scale no +graph_vlabel Pressure [${PRESSURE_UNIT}] +pressure.label Pressure + +multigraph wunderground_${STATION_ID}.solar_radiation +graph_title Solar radiation in \(.neighborhood) +graph_info Solar radiation ${STATION_INFO} +graph_category weather +graph_args -l 0 --base 1000 +graph_vlabel Solar radiation [W/m^2] +solarRadiation.label Solar radiation + +multigraph wunderground_${STATION_ID}.temperature +temp.label Temperature +graph_title Temperature in \(.neighborhood) +graph_info Temperature ${STATION_INFO} +graph_category weather +graph_vlabel Temperature [${TEMP_UNIT}] +temp.label Temperature + +dewpt.label Dew point +dewpt.info Temperature to which air must be cooled to become saturated with water vapor. When cooled further, the airborne water vapor will condense to form liquid water (dew). When air cools to its dew point through contact with a surface that is colder than the air, water will condense on the surface. When the temperature is below the freezing point of water, the dew point is called the frost point, as frost is formed rather than dew. + +windChill.label Wind chill +windChill.info Represent the lowering of body temperature due to the passing-flow of lower-temperature air. Wind chill numbers are always lower than the air temperature for values where the formula is valid. When the apparent temperature is higher than the air temperature, the heat index is used instead. +windChill.warning ${WIND_CHILL_CAUTION} +windChill.critical ${WIND_CHILL_DANGER} +windChillCaution.label Wind chill Caution +windChillCaution.info Danger of frostbite +windChillCaution.colour 5358f6 +windChillCaution.line ${WIND_CHILL_CAUTION} +windChillDanger.label Wind chill Danger +windChillDanger.info Great danger of frostbite +windChillDanger.colour 5c1ff5 +windChillDanger.line ${WIND_CHILL_DANGER} + +heatIndex.label Heat index +heatIndex.info Index that combines air temperature and relative humidity, in shaded areas, to posit a human-perceived equivalent temperature, as how hot it would feel if the humidity were some other value in the shade. +heatIndex.warning ${HEAT_INDEX_EXTREME_CAUTION} +heatIndex.critical ${HEAT_INDEX_DANGER} +heatIndexCaution.label Heat index Caution +heatIndexCaution.info Fatigue is possible with prolonged exposure and activity. Continuing activity could result in heat cramps. +heatIndexCaution.colour ffff66 +heatIndexCaution.line ${HEAT_INDEX_CAUTION} +heatIndexECaution.label Heat index Extreme Caution +heatIndexECaution.info Heat cramps and heat exhaustion are possible. Continuing activity could result in heat stroke. +heatIndexECaution.colour ffd700 +heatIndexECaution.line ${HEAT_INDEX_EXTREME_CAUTION} +heatIndexDanger.label Heat index Danger +heatIndexDanger.info Heat cramps and heat exhaustion are likely; heat stroke is probable with continued activity. +heatIndexDanger.colour ff8c00 +heatIndexDanger.line ${HEAT_INDEX_DANGER} +heatIndexEDanger.label Heat index Extreme Danger +heatIndexEDanger.info Heat stroke is imminent. +heatIndexEDanger.colour ff0000 +heatIndexEDanger.line ${HEAT_INDEX_EXTREME_DANGER} + +multigraph wunderground_${STATION_ID}.uv_index +graph_title UV Index in \(.neighborhood) +graph_info UV index ${STATION_INFO} +graph_category weather +graph_args -l 0 +graph_vlabel UV index +uv.label UV index +uv.min 0 +uv.warning 5 +uv.critical 7 +moderate.label Moderate +moderate.info Stay in shade near midday when the Sun is strongest. If outdoors, wear Sun protective clothing, a wide-brimmed hat, and UV-blocking sunglasses. Generously apply broad spectrum SPF 30+ sunscreen every 1.5 hours, even on cloudy days, and after swimming or sweating. Bright surfaces, such as sand, water, and snow, will increase UV exposure. +moderate.colour fff300 +moderate.line 3 +high.label High +high.info Reduce time in the Sun between 10 a.m. and 4 p.m. If outdoors, seek shade and wear Sun protective clothing, a wide-brimmed hat, and UV-blocking sunglasses. Generously apply broad spectrum SPF 30+ sunscreen every 1.5 hours, even on cloudy days, and after swimming or sweating. Bright surfaces, such as sand, water, and snow, will increase UV exposure. +high.colour f18b00 +high.line 6 +veryhigh.label Very high +veryhigh.info Minimize Sun exposure between 10 a.m. and 4 p.m. If outdoors, seek shade and wear Sun protective clothing, a wide-brimmed hat, and UV-blocking sunglasses. Generously apply broad spectrum SPF 30+ sunscreen every 1.5 hours, even on cloudy days, and after swimming or sweating. Bright surfaces, such as sand, water, and snow, will increase UV exposure. +veryhigh.colour e53210 +veryhigh.line 8 +extreme.label Extreme +extreme.info Try to avoid Sun exposure between 10 a.m. and 4 p.m. If outdoors, seek shade and wear Sun protective clothing, a wide-brimmed hat, and UV-blocking sunglasses. Generously apply broad spectrum SPF 30+ sunscreen every 1.5 hours, even on cloudy days, and after swimming or sweating. Bright surfaces, such as sand, water, and snow, will increase UV exposure. +extreme.colour b567a4 +extreme.line 11 + +multigraph wunderground_${STATION_ID}.wind +graph_title Wind Speed in \(.neighborhood) +graph_info Wind speed and gusts ${STATION_INFO} +graph_category weather +graph_args -l 0 --base 1000 +graph_vlabel Wind speed [${SPEED_UNIT}] +windSpeed.label Wind speed +windGust.label Wind gusts + +multigraph wunderground_${STATION_ID}.wind_direction +graph_title Wind Direction in \(.neighborhood) +graph_info Wind direction ${STATION_INFO} +graph_category weather +graph_args --base 1000 -l 0 --upper-limit 360 +graph_vlabel Wind [°] +winddir.label Wind origin +winddir.min 0 +winddir.max 360 +winddir.line 0 +north.label North +north.colour COLOUR0 +north.line 360 +east.label East +east.colour COLOUR1 +east.line 90 +south.label South +south.colour COLOUR2 +south.line 180 +west.label West +west.colour COLOUR9 +west.line 270 +\"" +} + +get_data() { + fetch "${API_URL}" | jq -r ".observations[0] + | @text \" +multigraph wunderground_${STATION_ID} +temp.value \(.${UNITS}.temp) +windChill.value \(.${UNITS}.windChill) +heatIndex.value \(.${UNITS}.heatIndex) +uv.value \(.uv) +precipRate.value \(.${UNITS}.precipRate) + +multigraph wunderground_${STATION_ID}.air_humidity +humidity.value \(.humidity) + +multigraph wunderground_${STATION_ID}.location +lon.value \(.lon) +lat.value \(.lat) +elev.value \(.${UNITS}.elev) + +multigraph wunderground_${STATION_ID}.precipitation +precipRate.value \(.${UNITS}.precipRate) +avgRate.value \(.${UNITS}.precipTotal*100 | round) +avgRate.extinfo Immediate precipitation: \(.${UNITS}.precipRate) ${PRECIPITATION_UNIT}/h; Daily total: \(.${UNITS}.precipTotal) ${PRECIPITATION_UNIT} + +multigraph wunderground_${STATION_ID}.air_pressure +pressure.value \(.${UNITS}.pressure) + +multigraph wunderground_${STATION_ID}.solar_radiation +solarRadiation.value \(.solarRadiation) + +multigraph wunderground_${STATION_ID}.temperature +temp.value \(.${UNITS}.temp) +dewpt.value \(.${UNITS}.dewpt) +windChill.value \(.${UNITS}.windChill) +heatIndex.value \(.${UNITS}.heatIndex) + +multigraph wunderground_${STATION_ID}.uv_index +uv.value \(.uv) + +multigraph wunderground_${STATION_ID}.wind +windSpeed.value \(.${UNITS}.windSpeed) +windGust.value \(.${UNITS}.windGust) + +multigraph wunderground_${STATION_ID}.wind_direction +winddir.value \(.winddir) +\"" | sed 's/ null$/U/' +} + +main () { + check_deps + + case ${1:-} in + config) + config + if [ "${MUNIN_CAP_DIRTYCONFIG:-0}" = "1" ]; then + get_data + fi + ;; + *) + get_data + ;; + esac +} + +main "${1:-}"