├── images
├── na.png
├── PM25-1.jpg
├── PM25-2.jpg
├── blank.png
├── done.png
├── readme.png
├── wi-rain.png
├── wi-windy.png
├── battery-bad.png
├── battery-na.png
├── blue-ball.jpg
├── button-red.png
├── options-red.png
├── battery-good.png
├── blue-ball-100.jpg
├── blue-ball-200.jpg
├── button-green.png
├── instructions.png
├── options-green.png
├── HE AWS Dashboard.jpg
├── No-Color-Option.jpg
├── checkMarkGreen2.png
├── wi-direction-up.png
├── hubitat-dashboard.jpg
├── weathericons
│ ├── icon0.png
│ ├── icon1.png
│ ├── icon2.png
│ ├── icon3.png
│ ├── icon4.png
│ ├── icon5.png
│ ├── icon6.png
│ ├── icon7.png
│ ├── icon8.png
│ ├── icon9.png
│ ├── icon10.png
│ ├── icon11.png
│ ├── icon12.png
│ ├── icon13.png
│ ├── icon14.png
│ ├── icon15.png
│ ├── icon16.png
│ ├── icon17.png
│ ├── icon18.png
│ ├── icon19.png
│ ├── icon20.png
│ ├── icon21.png
│ ├── icon22.png
│ ├── icon23.png
│ ├── icon24.png
│ ├── icon25.png
│ ├── icon26.png
│ ├── icon27.png
│ ├── icon28.png
│ ├── icon29.png
│ ├── icon30.png
│ ├── icon31.png
│ ├── icon32.png
│ ├── icon33.png
│ ├── icon34.png
│ ├── icon35.png
│ ├── icon36.png
│ ├── icon37.png
│ ├── icon38.png
│ ├── icon39.png
│ ├── icon40.png
│ ├── icon41.png
│ ├── icon42.png
│ ├── icon43.png
│ ├── icon44.png
│ ├── icon45.png
│ ├── icon46.png
│ └── icon47.png
├── wi-direction-down.png
├── wi-direction-left.png
├── wi-direction-right.png
├── moon-phase-symbol-0.png
├── moon-phase-symbol-1.png
├── moon-phase-symbol-10.png
├── moon-phase-symbol-11.png
├── moon-phase-symbol-12.png
├── moon-phase-symbol-13.png
├── moon-phase-symbol-14.png
├── moon-phase-symbol-15.png
├── moon-phase-symbol-16.png
├── moon-phase-symbol-17.png
├── moon-phase-symbol-18.png
├── moon-phase-symbol-19.png
├── moon-phase-symbol-2.png
├── moon-phase-symbol-20.png
├── moon-phase-symbol-21.png
├── moon-phase-symbol-22.png
├── moon-phase-symbol-23.png
├── moon-phase-symbol-24.png
├── moon-phase-symbol-25.png
├── moon-phase-symbol-26.png
├── moon-phase-symbol-27.png
├── moon-phase-symbol-28.png
├── moon-phase-symbol-29.png
├── moon-phase-symbol-3.png
├── moon-phase-symbol-4.png
├── moon-phase-symbol-5.png
├── moon-phase-symbol-6.png
├── moon-phase-symbol-7.png
├── moon-phase-symbol-8.png
├── moon-phase-symbol-9.png
├── wi-direction-up-left.png
├── wi-direction-up-right.png
├── wi-direction-down-left.png
├── wi-direction-down-right.png
├── Ambient Preferences Settings.jpg
└── Ambient Weather Station Data Key Import Selector.jpg
├── .gitignore
├── hubitat
├── bundles
│ └── AmbientWeatherStationLibrary.zip
├── packageManifest.json
├── devicetypes
│ └── kurtsanders
│ │ ├── ambient-weather-station-remote-sensor.src
│ │ └── ambient-weather-station remote-sensor.groovy
│ │ ├── ambient-particulate-monitor.src
│ │ └── ambient-particulate-monitor.groovy
│ │ └── ambient-weather-station.src
│ │ └── ambient-weather-station.groovy
├── library
│ └── AWSLibrary.groovy
└── smartapps
│ └── kurtsanders
│ └── ambient-weather-station.src
│ └── ambient-weather-station.groovy
├── repository.json
├── README.md
└── License.md
/images/na.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/na.png
--------------------------------------------------------------------------------
/images/PM25-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/PM25-1.jpg
--------------------------------------------------------------------------------
/images/PM25-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/PM25-2.jpg
--------------------------------------------------------------------------------
/images/blank.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/blank.png
--------------------------------------------------------------------------------
/images/done.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/done.png
--------------------------------------------------------------------------------
/images/readme.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/readme.png
--------------------------------------------------------------------------------
/images/wi-rain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-rain.png
--------------------------------------------------------------------------------
/images/wi-windy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-windy.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .DS_Store
3 | .Spotlight-V100
4 | .DS_Store1
5 | *.groovy___jb_old___
6 | /hubitat/hpm
7 |
--------------------------------------------------------------------------------
/images/battery-bad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/battery-bad.png
--------------------------------------------------------------------------------
/images/battery-na.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/battery-na.png
--------------------------------------------------------------------------------
/images/blue-ball.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/blue-ball.jpg
--------------------------------------------------------------------------------
/images/button-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/button-red.png
--------------------------------------------------------------------------------
/images/options-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/options-red.png
--------------------------------------------------------------------------------
/images/battery-good.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/battery-good.png
--------------------------------------------------------------------------------
/images/blue-ball-100.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/blue-ball-100.jpg
--------------------------------------------------------------------------------
/images/blue-ball-200.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/blue-ball-200.jpg
--------------------------------------------------------------------------------
/images/button-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/button-green.png
--------------------------------------------------------------------------------
/images/instructions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/instructions.png
--------------------------------------------------------------------------------
/images/options-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/options-green.png
--------------------------------------------------------------------------------
/images/HE AWS Dashboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/HE AWS Dashboard.jpg
--------------------------------------------------------------------------------
/images/No-Color-Option.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/No-Color-Option.jpg
--------------------------------------------------------------------------------
/images/checkMarkGreen2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/checkMarkGreen2.png
--------------------------------------------------------------------------------
/images/wi-direction-up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-direction-up.png
--------------------------------------------------------------------------------
/images/hubitat-dashboard.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/hubitat-dashboard.jpg
--------------------------------------------------------------------------------
/images/weathericons/icon0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon0.png
--------------------------------------------------------------------------------
/images/weathericons/icon1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon1.png
--------------------------------------------------------------------------------
/images/weathericons/icon2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon2.png
--------------------------------------------------------------------------------
/images/weathericons/icon3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon3.png
--------------------------------------------------------------------------------
/images/weathericons/icon4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon4.png
--------------------------------------------------------------------------------
/images/weathericons/icon5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon5.png
--------------------------------------------------------------------------------
/images/weathericons/icon6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon6.png
--------------------------------------------------------------------------------
/images/weathericons/icon7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon7.png
--------------------------------------------------------------------------------
/images/weathericons/icon8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon8.png
--------------------------------------------------------------------------------
/images/weathericons/icon9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon9.png
--------------------------------------------------------------------------------
/images/wi-direction-down.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-direction-down.png
--------------------------------------------------------------------------------
/images/wi-direction-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-direction-left.png
--------------------------------------------------------------------------------
/images/wi-direction-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-direction-right.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-0.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-1.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-10.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-11.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-12.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-13.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-14.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-15.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-16.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-17.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-18.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-19.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-2.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-20.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-21.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-22.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-23.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-24.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-25.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-26.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-27.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-27.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-28.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-28.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-29.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-3.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-4.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-5.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-6.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-7.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-8.png
--------------------------------------------------------------------------------
/images/moon-phase-symbol-9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/moon-phase-symbol-9.png
--------------------------------------------------------------------------------
/images/weathericons/icon10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon10.png
--------------------------------------------------------------------------------
/images/weathericons/icon11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon11.png
--------------------------------------------------------------------------------
/images/weathericons/icon12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon12.png
--------------------------------------------------------------------------------
/images/weathericons/icon13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon13.png
--------------------------------------------------------------------------------
/images/weathericons/icon14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon14.png
--------------------------------------------------------------------------------
/images/weathericons/icon15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon15.png
--------------------------------------------------------------------------------
/images/weathericons/icon16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon16.png
--------------------------------------------------------------------------------
/images/weathericons/icon17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon17.png
--------------------------------------------------------------------------------
/images/weathericons/icon18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon18.png
--------------------------------------------------------------------------------
/images/weathericons/icon19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon19.png
--------------------------------------------------------------------------------
/images/weathericons/icon20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon20.png
--------------------------------------------------------------------------------
/images/weathericons/icon21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon21.png
--------------------------------------------------------------------------------
/images/weathericons/icon22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon22.png
--------------------------------------------------------------------------------
/images/weathericons/icon23.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon23.png
--------------------------------------------------------------------------------
/images/weathericons/icon24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon24.png
--------------------------------------------------------------------------------
/images/weathericons/icon25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon25.png
--------------------------------------------------------------------------------
/images/weathericons/icon26.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon26.png
--------------------------------------------------------------------------------
/images/weathericons/icon27.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon27.png
--------------------------------------------------------------------------------
/images/weathericons/icon28.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon28.png
--------------------------------------------------------------------------------
/images/weathericons/icon29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon29.png
--------------------------------------------------------------------------------
/images/weathericons/icon30.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon30.png
--------------------------------------------------------------------------------
/images/weathericons/icon31.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon31.png
--------------------------------------------------------------------------------
/images/weathericons/icon32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon32.png
--------------------------------------------------------------------------------
/images/weathericons/icon33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon33.png
--------------------------------------------------------------------------------
/images/weathericons/icon34.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon34.png
--------------------------------------------------------------------------------
/images/weathericons/icon35.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon35.png
--------------------------------------------------------------------------------
/images/weathericons/icon36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon36.png
--------------------------------------------------------------------------------
/images/weathericons/icon37.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon37.png
--------------------------------------------------------------------------------
/images/weathericons/icon38.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon38.png
--------------------------------------------------------------------------------
/images/weathericons/icon39.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon39.png
--------------------------------------------------------------------------------
/images/weathericons/icon40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon40.png
--------------------------------------------------------------------------------
/images/weathericons/icon41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon41.png
--------------------------------------------------------------------------------
/images/weathericons/icon42.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon42.png
--------------------------------------------------------------------------------
/images/weathericons/icon43.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon43.png
--------------------------------------------------------------------------------
/images/weathericons/icon44.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon44.png
--------------------------------------------------------------------------------
/images/weathericons/icon45.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon45.png
--------------------------------------------------------------------------------
/images/weathericons/icon46.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon46.png
--------------------------------------------------------------------------------
/images/weathericons/icon47.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/weathericons/icon47.png
--------------------------------------------------------------------------------
/images/wi-direction-up-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-direction-up-left.png
--------------------------------------------------------------------------------
/images/wi-direction-up-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-direction-up-right.png
--------------------------------------------------------------------------------
/images/wi-direction-down-left.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-direction-down-left.png
--------------------------------------------------------------------------------
/images/wi-direction-down-right.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/wi-direction-down-right.png
--------------------------------------------------------------------------------
/images/Ambient Preferences Settings.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/Ambient Preferences Settings.jpg
--------------------------------------------------------------------------------
/hubitat/bundles/AmbientWeatherStationLibrary.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/hubitat/bundles/AmbientWeatherStationLibrary.zip
--------------------------------------------------------------------------------
/images/Ambient Weather Station Data Key Import Selector.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/HEAD/images/Ambient Weather Station Data Key Import Selector.jpg
--------------------------------------------------------------------------------
/repository.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "Kurt Sanders",
3 | "gitHubUrl": "https://github.com/KurtSanders",
4 | "payPalUrl": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=TJDQJASTD8JKU&source=url",
5 | "packages": [
6 | {
7 | "name": "Ambient Weather Station",
8 | "category": "Integrations",
9 | "location": "https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/master/hubitat/packageManifest.json",
10 | "description": "Integrate your Internet connected Ambient Weather Station system into the Hubitat environment.",
11 | "tags": [ "Monitoring", "Weather","Temperature & Humidity", "Water" ]
12 | },
13 | {
14 | "name": "BitBar Output App",
15 | "category": "Convenience",
16 | "location": "https://raw.githubusercontent.com/KurtSanders/STBitBarApp-V2/master/hubitat/packageManifest.json",
17 | "description": "Monitor and control Hubitat devices, sensors, Hubitat Safety Monitor, Modes & Routines from the Apple MacOS Menu Bar.",
18 | "tags": [ "Monitoring","Tools & Utilities", "Notifications" ]
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/hubitat/packageManifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "packageName": "Ambient Weather Station",
3 | "author": "Kurt Sanders",
4 | "minimumHEVersion": "2.4.3",
5 | "dateReleased": "2025-10-24",
6 | "gitHubUrl": "https://github.com/KurtSanders/STAmbientWeather",
7 | "documentationLink": "https://github.com/KurtSanders/STAmbientWeather/blob/master/README.md",
8 | "licenseFile": "",
9 | "version": "6.7.3",
10 | "payPalUrl": "https://www.paypal.com/donate/?hosted_button_id=E4WXT86RTPXDC",
11 | "releaseNotes": "Version 6.7.3\n1. Bug Fix for 'Illegal Format Conversion' of Solar Radiation when value is 0",
12 | "bundles": [
13 | {
14 | "id": "db82c41e-d7e5-4de5-9691-b432b2255500",
15 | "name": "Ambient Weather Station Library",
16 | "namespace": "kurtsanders",
17 | "location": "https://github.com/KurtSanders/STAmbientWeather/raw/master/hubitat/bundles/AmbientWeatherStationLibrary.zip",
18 | "required": true
19 | }
20 | ],
21 | "apps": [
22 | {
23 | "id": "81c44d2b-623a-4428-9229-765370eabc97",
24 | "name": "Ambient Weather Station",
25 | "namespace": "kurtsanders",
26 | "location": "https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/master/hubitat/smartapps/kurtsanders/ambient-weather-station.src/ambient-weather-station.groovy",
27 | "required": true,
28 | "primary": true,
29 | "oauth": false
30 | }
31 | ],
32 | "drivers": [
33 | {
34 | "id": "5cf77327-f54d-4c2b-be9a-4ec31a3bfcd6",
35 | "name": "Ambient Weather Station",
36 | "namespace": "kurtsanders",
37 | "location": "https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/master/hubitat/devicetypes/kurtsanders/ambient-weather-station.src/ambient-weather-station.groovy",
38 | "required": true
39 | },
40 | {
41 | "id": "ce03d310-92cc-4848-8ba3-42255efe3a1b",
42 | "name": "Ambient Weather Station Remote Sensor",
43 | "namespace": "kurtsanders",
44 | "location": "https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/master/hubitat/devicetypes/kurtsanders/ambient-weather-station-remote-sensor.src/ambient-weather-station%20remote-sensor.groovy",
45 | "required": true
46 | },
47 | {
48 | "id": "237364e5-039c-4b7b-ad81-dec3cb251659",
49 | "name": "Ambient Particulate Monitor",
50 | "namespace": "kurtsanders",
51 | "location": "https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/master/hubitat/devicetypes/kurtsanders/ambient-particulate-monitor.src/ambient-particulate-monitor.groovy",
52 | "required": true
53 | }
54 | ]
55 | }
--------------------------------------------------------------------------------
/hubitat/devicetypes/kurtsanders/ambient-weather-station-remote-sensor.src/ambient-weather-station remote-sensor.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018, 2019, 2022, 2023, 2024, 2025 SanderSoft
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at:
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing permissions and limitations under the License.
12 | *
13 | * Ambient Weather Station Remote Sensor
14 | *
15 | * Author: Kurt Sanders, SanderSoft™
16 | */
17 |
18 | import groovy.time.*
19 | import java.text.SimpleDateFormat;
20 | import groovy.transform.Field
21 | #include kurtsanders.AWSLibrary
22 |
23 | @Field static String PARENT_DEVICE_NAME = "Ambient Weather Station Remote Sensor"
24 | @Field static final String VERSION = "6.3.0"
25 |
26 | metadata {
27 | definition (name: PARENT_DEVICE_NAME,
28 | namespace: NAMESPACE,
29 | author: AUTHOR_NAME,
30 | importUrl: "https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/master/hubitat/drivers/ambient-weather-station%20remote-sensor.driver"
31 | ) {
32 | capability "TemperatureMeasurement"
33 | capability "RelativeHumidityMeasurement"
34 | capability "Sensor"
35 | capability "Battery"
36 | capability "Refresh"
37 |
38 | attribute "date", "string"
39 | attribute "lastSTupdate", "string"
40 | attribute "version", "string"
41 | attribute "feelsLike_display", "string"
42 | attribute "feelsLike", "number"
43 | attribute "dewPoint_display", "string"
44 | attribute "dewPoint", "number"
45 | attribute "dewpoint", "number"
46 |
47 |
48 | command "refresh"
49 | command "clearAllDeviceCurrentStates"
50 |
51 | }
52 | }
53 |
54 | def deleteDeviceData() {
55 | logInfo "${device.name}: deleteDeviceData..."
56 | clearAllDeviceCurrentStates(false)
57 | }
58 |
59 | def clearAllDeviceCurrentStates(refresh=true) {
60 | logInfo "Clearing current states of this device..."
61 | device.currentStates.eachWithIndex {item, index ->
62 | device.deleteCurrentState(item.name)
63 | logInfo "Deleted ${index}. → ${item.name}"
64 | }
65 | if (refresh) parent.refresh()
66 | }
67 |
68 | def refresh() {
69 | Date now = new Date()
70 | def timeString = now.format("EEE MMM dd h:mm:ss a", location.timeZone)
71 | logInfo "User requested a 'Manual Refresh' from Ambient Weather Station device, sending refresh() request to parent smartApp"
72 | parent.refresh()
73 | }
74 | def initialize() {
75 | checkLogLevel()
76 | }
77 | def installed() {
78 | checkLogLevel()
79 | }
80 | def updated() {
81 | checkLogLevel()
82 | }
83 |
--------------------------------------------------------------------------------
/hubitat/devicetypes/kurtsanders/ambient-particulate-monitor.src/ambient-particulate-monitor.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018, 2019, 2021, 2022 SanderSoft
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at:
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing permissions and limitations under the License.
12 | *
13 | * Ambient Particulate Monitor
14 | *
15 | * Author: Kurt Sanders, SanderSoft™
16 | */
17 |
18 | import groovy.time.*
19 | import java.text.SimpleDateFormat;
20 | import groovy.transform.Field
21 | #include kurtsanders.AWSLibrary
22 |
23 |
24 | @Field static String PARENT_DEVICE_NAME = "Ambient Particulate Monitor"
25 | @Field static final String VERSION = "6.3.0"
26 |
27 | metadata {
28 | definition (name: PARENT_DEVICE_NAME,
29 | namespace: NAMESPACE,
30 | author: AUTHOR_NAME,
31 | importUrl: "https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/master/hubitat/drivers/ambient-particulate-monitor.driver"
32 | ) {
33 | capability "Sensor"
34 | capability "Battery"
35 | capability "Refresh"
36 |
37 | attribute "aqi_pm25_24h_aqin", "number"
38 | attribute "aqi_pm25_24h", "number"
39 | attribute "aqi_pm25_aqin", "number"
40 | attribute "aqi_pm25", "number"
41 | attribute "co2_in_24h_aqin", "number"
42 | attribute "co2_in_aqin", "number"
43 | attribute "date", "string"
44 | attribute "lastSTupdate", "string"
45 | attribute "pm_in_humidity_aqin", "number"
46 | attribute "pm_in_temp_aqin", "number"
47 | attribute "pm10_in_24h_aqin", "number"
48 | attribute "pm10_in_aqin", "number"
49 | attribute "pm25_24h", "number"
50 | attribute "pm25_in_24h_aqin", "number"
51 | attribute "pm25_in_aqin", "number"
52 | attribute "pm25", "number"
53 | attribute "version", "string"
54 |
55 | command "refresh"
56 | command "clearAllDeviceCurrentStates"
57 | }
58 | }
59 | def refresh() {
60 | Date now = new Date()
61 | def timeString = now.format("EEE MMM dd h:mm:ss a", location.timeZone)
62 | sendEvent(name: "lastSTupdate", value: "Cloud Refresh Requested at\n${timeString}...", "displayed":false)
63 | sendEvent(name: "aqi", value: "Refresh Requested at ${timeString}...", displayed: false)
64 | parent.refresh()
65 | }
66 |
67 | def deleteDeviceData() {
68 | logInfo "${device.name}: deleteDeviceData..."
69 | clearAllDeviceCurrentStates(false)
70 | }
71 |
72 | def clearAllDeviceCurrentStates(refresh=true) {
73 | logInfo "Clearing current states of this device..."
74 | device.currentStates.eachWithIndex {item, index ->
75 | device.deleteCurrentState(item.name)
76 | logInfo "Deleted ${index}. → ${item.name}"
77 | }
78 | if (refresh) parent.refresh()
79 | }
80 |
81 | def initialize() {
82 | checkLogLevel()
83 | }
84 | def installed() {
85 | log.debug "Ambient Particulate Monitor/AQIN device created"
86 | checkLogLevel()
87 | }
88 | def updated() {
89 | checkLogLevel()
90 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ambient Weather® Station™ (AWS)
2 | *Hubitat® Integration for Ambient Weather® Stations by SanderSoft™*
3 | ### Hubitat AWS Suite Version: 6.7.1
4 |
5 |
[Change-log & Version Release Features](https://github.com/KurtSanders/STAmbientWeather/wiki/Features-by-Version)
6 |
7 | #### You must install [Hubitat Package Manager](https://hubitatpackagemanager.hubitatcommunity.com/) and install Ambient Weather Station application via HPM. Search in HPM for the keyword 'Weather'.
8 |
9 | [Hubitat Community Support WebLink](https://community.hubitat.com/t/release-ambient-weather-station-app/128838)
10 |
11 | ---
12 |
13 | ### Description:
14 |
15 |
16 |
17 | A custom Hubitat® SmartApp integrates weather/environmental data generated from one's personal [Ambient Weather® station](https://www.ambientweather.com/ambientnet.html), sensors, and accessories.
18 |
19 | This Hubitat® application provides integration to your [Ambientweather.net](https://ambientweather.net/) weather data via the [AmbientWeather API](https://ambientweather.docs.apiary.io/#). The HE user can set the polling rate of their weather and sensor data from either a manual or automatic refresh cycle (1 min to 180 mins (3 hours)).
20 |
21 | ### Supported Ambient Weather Station Brand Devices and Accessories:
22 |
23 | * [Ambient Weather Station](https://ambientweather.com/ws-2902-smart-weather-station) with Internet Access
24 | * [PM25](https://ambientweather.com/ampm25.html)/[AQIN](https://ambientweather.com/indoor-wireless-air-quality-monitor-aqin) Sensors
25 | * Soil Sensors
26 | * Wind Sensor
27 | * Rain Sensor
28 | * Up to 8 Weather Remote Sensors
29 |
30 | #### AWS Settings Examples
31 | * Preference Screens
32 |
33 |
34 |
35 | * Import Selection
36 |
37 |
38 |
39 |
40 | #### Ambient Weather Station Device Information :arrow_right: Hubitat™
41 |
42 | [View Ambient Weather Station's Device Data Specs](https://github.com/ambient-weather/api-docs/wiki/Device-Data-Specs)
43 |
44 | | HE Capability | HE Attribute | Reported Type |
45 | |:------------:|:-------------------|:-------------------:|
46 | |capability|Battery|Battery|
47 | |capability|IlluminanceMeasurement|Illuminance|
48 | |capability|Refresh|Refresh|
49 | |capability|RelativeHumidityMeasurement|Humidity|
50 | |capability|Sensor| N/A |
51 | |capability|TemperatureMeasurement|Temperature|
52 | |capability|UltravioletIndex|Ultraviolet|
53 |
54 | | HE Attribute | HE Device State Data Key | Reported Type |
55 | |:------------:|:-------------------|:-------------------:|
56 | |attribute|baromabsin_display|string|
57 | |attribute|baromabsin|number|
58 | |attribute|baromrelin_display|string|
59 | |attribute|baromrelin|number|
60 | |attribute|batt_lightning|number|
61 | |attribute|city|string|
62 | |attribute|dailyrainin_display|string|
63 | |attribute|dailyrainin|number|
64 | |attribute|date|string|
65 | |attribute|date|string|
66 | |attribute|dateutc|string|
67 | |attribute|dewPoint_display|string|
68 | |attribute|dewpoint|number|
69 | |attribute|dewPoint|number|
70 | |attribute|eventrainin_display|string|
71 | |attribute|eventrainin|number|
72 | |attribute|feelsLike_display|string|
73 | |attribute|feelslike|number|
74 | |attribute|feelsLike|number|
75 | |attribute|hourlyrainin_display|string|
76 | |attribute|hourlyrainin|number|
77 | |attribute|humidity_display|string|
78 | |attribute|humidityin_display|string|
79 | |attribute|humidityin|number|
80 | |attribute|lastRain|string|
81 | |attribute|lastRainDuration|string|
82 | |attribute|lastSTupdate|string|
83 | |attribute|lightning_day|number|
84 | |attribute|lightning_distance|number|
85 | |attribute|lightning_hour|number|
86 | |attribute|lightning_time|number|
87 | |attribute|location|string|
88 | |attribute|macAddress|string|
89 | |attribute|maxdailygust_display|string|
90 | |attribute|maxdailygust|number|
91 | |attribute|monthlyrainin_display|string|
92 | |attribute|monthlyrainin|number|
93 | |attribute|pwsName|string|
94 | |attribute|scheduleFreqMin|string|
95 | |attribute|solarradiation_display|string|
96 | |attribute|solarradiation|number|
97 | |attribute|tempf_display|string|
98 | |attribute|tempinf_display|string|
99 | |attribute|tempinf|number|
100 | |attribute|totalrainin_display|string|
101 | |attribute|totalrainin|number|
102 | |attribute|ultravioletIndexDisplay|string|
103 | |attribute|version|string|
104 | |attribute|weeklyrainin_display|string|
105 | |attribute|weeklyrainin|number|
106 | |attribute|wind_cardinal|string|
107 | |attribute|wind|number|
108 | |attribute|winddir|string|
109 | |attribute|windDirection|number|
110 | |attribute|winddirection|string|
111 | |attribute|windgustmph_display|string|
112 | |attribute|windgustmph|number|
113 | |attribute|windSpeed|number|
114 | |attribute|windspeedmph_display|string|
115 | |attribute|windspeedmph|number|
116 | |attribute|windVector|string|
117 |
118 |
119 | ### Requirements:
120 | * You must create your own and have access to the following **private data strings** displayed at your [AWS Account](https://ambientweather.net/account) and [My Devices](https://ambientweather.net/devices) webpages.
121 | * [Locate your weather station's MAC address](https://ambientweather.net/devices)
122 | * [Locate your Ambient API key](https://ambientweather.net/account)
123 |
124 |
--------------------------------------------------------------------------------
/hubitat/devicetypes/kurtsanders/ambient-weather-station.src/ambient-weather-station.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2018, 2019, 2021, 2022, 2023, 2024, 2025 SanderSoft
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at:
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing permissions and limitations under the License.
12 | *
13 | * Ambient Weather Station
14 | *
15 | * Author: Kurt Sanders, SanderSoft™
16 | */
17 | import groovy.time.*
18 | import java.text.SimpleDateFormat;
19 | import groovy.transform.Field
20 | #include kurtsanders.AWSLibrary
21 |
22 | @Field static String PARENT_DEVICE_NAME = "Ambient Weather Station"
23 | @Field static final String VERSION = "6.7.0"
24 |
25 | metadata {
26 | definition (name: PARENT_DEVICE_NAME,
27 | namespace: NAMESPACE,
28 | author: AUTHOR_NAME,
29 | importUrl: "https://raw.githubusercontent.com/KurtSanders/STAmbientWeather/master/hubitat/drivers/ambient-weather-station.driver"
30 | ) {
31 | capability "IlluminanceMeasurement"
32 | capability "TemperatureMeasurement"
33 | capability "RelativeHumidityMeasurement"
34 | capability "Sensor"
35 | capability "Refresh"
36 | capability "UltravioletIndex"
37 | capability "Battery"
38 |
39 | // Start of Ambient Weather API Rest MAP
40 | // Actual numeric values from Ambient Weather API non rounded
41 | attribute "wind", "number" //SharpTool.io
42 | attribute "windDirection", "number" //Hubitat OpenWeather
43 | attribute "windSpeed", "number" //Hubitat OpenWeather
44 | attribute "wind_cardinal", "string"
45 |
46 | // Display values from Ambient Weather API rounded and with {optional} units
47 | attribute "windspeedmph_display", "string"
48 | attribute "windgustmph_display", "string"
49 | attribute "maxdailygust_display", "string"
50 | attribute "tempf_display", "string"
51 | attribute "hourlyrainin_display", "string"
52 | attribute "eventrainin_display", "string"
53 | attribute "dailyrainin_display", "string"
54 | attribute "weeklyrainin_display", "string"
55 | attribute "monthlyrainin_display", "string"
56 | attribute "totalrainin_display", "string"
57 | attribute "baromrelin_display", "string"
58 | attribute "baromabsin_display", "string"
59 | attribute "humidity_display", "string"
60 | attribute "tempinf_display", "string"
61 | attribute "humidityin_display", "string"
62 | attribute "solarradiation_display", "string"
63 | attribute "feelsLike_display", "string"
64 | attribute "feelsLikein_display", "string"
65 | attribute "dewPoint_display", "string"
66 |
67 | // Weather Station meta data
68 | attribute "elevation", "number"
69 | attribute "location", "string"
70 | attribute "lat", "number"
71 | attribute "lon", "number"
72 | attribute "macAddress", "string"
73 | attribute "pwsName", "string"
74 |
75 |
76 | // Weather Station device data
77 | attribute "baromabsin", "number"
78 | attribute "baromrelin", "number"
79 | attribute "dailyrainin", "number"
80 | attribute "date", "string"
81 | attribute "dateutc", "string"
82 | attribute "dewPoint", "number"
83 | attribute "dewpoint", "number"
84 | attribute "eventrainin", "number"
85 | attribute "feelsLike", "number"
86 | attribute "feelsLikein", "number"
87 | attribute "hourlyrainin", "number"
88 | attribute "humidityin", "number"
89 | attribute "lastRain", "string"
90 | attribute "lastRainDuration", "string"
91 | attribute "maxdailygust", "number"
92 | attribute "monthlyrainin", "number"
93 | attribute "solarradiation", "number"
94 | attribute "tempinf", "number"
95 | attribute "totalrainin", "number"
96 | attribute "weeklyrainin", "number"
97 | attribute "windVector", "string"
98 | attribute "winddir", "string"
99 | attribute "winddirection", "string"
100 | attribute "windgustmph", "number"
101 | attribute "windspeedmph", "number"
102 | attribute "ultravioletIndexDisplay", "string"
103 | attribute "lightning_day", "number"
104 | attribute "lightning_time", "number"
105 | attribute "lightning_distance", "number"
106 | attribute "lightning_hour", "number"
107 | attribute "batt_lightning", "number"
108 | // End of Ambient Weather API Rest MAP
109 |
110 | // Weather Forecast & Misc attributes
111 | attribute "lastSTupdate", "string"
112 | attribute "scheduleFreqMin", "number"
113 | attribute "version", "string"
114 | attribute "date", "string"
115 |
116 | // Commands
117 | command "refresh"
118 | command "clearAllDeviceCurrentStates"
119 | command "setPollingInterval", [[name:"Set AWS Polling Interval*", type:"ENUM", description:"Set AWS Polling Interval", constraints:POLLING_OPTIONS_MAP]]
120 |
121 | }
122 | }
123 |
124 | def initialize() {
125 | checkLogLevel()
126 | state.dataFieldsWiki = 'Wiki Device Data Specs Link'
127 | state.webLink = fmtDataFieldsWikiLink()
128 | }
129 |
130 | def installed() {
131 | initialize()
132 | }
133 |
134 | def updated() {
135 | initialize()
136 | }
137 |
138 | def deleteDeviceData() {
139 | logInfo "${device.name}: deleteDeviceData..."
140 | clearAllDeviceCurrentStates(false)
141 | }
142 |
143 | def clearAllDeviceCurrentStates(refresh=true) {
144 | logInfo "Clearing current states of this device..."
145 | device.currentStates.eachWithIndex {item, index ->
146 | device.deleteCurrentState(item.name)
147 | logInfo "Deleted ${index}. → ${item.name}"
148 | }
149 | if (refresh) parent.refresh()
150 | }
151 |
152 | def refresh() {
153 | Date now = new Date()
154 | def timeString = now.format("EEE MMM dd h:mm:ss a", location.timeZone)
155 | logInfo "User requested a 'Manual Refresh' from Ambient Weather Station device, sending refresh() request to parent smartApp"
156 | parent.refresh()
157 | }
158 |
159 | def setPollingInterval(pollingInterval) {
160 | def pollingKey = POLLING_OPTIONS_MAP.find { it.value == pollingInterval }?.key
161 | log.debug "Set pollingInterval = ${pollingInterval} (${pollingKey})"
162 | parent.setPollingInterval(pollingKey)
163 | }
164 |
165 | //Additional Preferences
166 | preferences {
167 | //Logging Options
168 | input name: "logLevel", type: "enum", title: fmtTitle("Logging Level"),
169 | description: fmtDesc("Logs selected level and above"), defaultValue: 0, options: LOG_LEVELS
170 | input name: "logLevelTime", type: "enum", title: fmtTitle("Logging Level Time"),
171 | description: fmtDesc("Time to enable Debug/Trace logging"),defaultValue: 0, options: LOG_TIMES
172 | //Help Link
173 | input name: "helpInfo", type: "hidden", title: fmtHelpInfo("Community Link")
174 | }
175 |
--------------------------------------------------------------------------------
/License.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/hubitat/library/AWSLibrary.groovy:
--------------------------------------------------------------------------------
1 | /*******************************************************************
2 | *** SanderSoft - Core App/Device Helpers ***
3 | /*******************************************************************/
4 |
5 |
6 | library (
7 | base: "app",
8 | author: "Kurt Sanders",
9 | category: "Apps",
10 | description: "Core functions for Ambient Weather Station Suite.",
11 | name: "AWSLibrary",
12 | namespace: "kurtsanders",
13 | documentationLink: "https://github.com/KurtSanders/STAmbientWeather/blob/master/README.md",
14 | version: "1.2.0",
15 | disclaimer: "This library is only for use with SanderSoft Apps and Drivers."
16 | )
17 |
18 | import groovy.json.*
19 | import groovy.json.JsonOutput
20 | import groovy.json.JsonSlurper
21 | import groovy.time.TimeCategory
22 | import groovy.transform.Field
23 | import hubitat.helper.RMUtils
24 | import java.text.DecimalFormat
25 | import java.text.SimpleDateFormat
26 | import java.util.Date
27 | import java.util.TimeZone
28 |
29 | @Field static String AUTHOR_NAME = "Kurt Sanders"
30 | @Field static String NAMESPACE = "kurtsanders"
31 | @Field static final String COMM_LINK = "https://community.hubitat.com/t/release-ambient-weather-station-app/128838"
32 | @Field static final String GITHUB_LINK = "https://github.com/KurtSanders/STAmbientWeather?tab=readme-ov-file#ambient-weather-station-aws"
33 | @Field static final String GITHUB_IMAGES_LINK = "https://raw.githubusercontent.com/kurtsanders/HubitatPackages/master/resources/images/"
34 | @Field static final String DEVICE_DATA_SPECS = "https://github.com/ambient-weather/api-docs/wiki/Device-Data-Specs"
35 | @Field static final Map POLLING_OPTIONS_MAP = ['0':'Off','1':'1 min','2':'2 mins','3':'3 mins','4':'4 mins','5':'5 mins','10':'10 mins','15':'15 mins','30':'Every ½ Hour','60':'Every Hour','120':'Every 2 Hours','180':'Every 3 Hours']
36 |
37 | def setLibraryVersion() {
38 | state.libraryVersion = "1.2.0"
39 | }
40 |
41 | def uninstalled() {
42 | sendLocationEvent(name: "updateVersionInfo", value: "${app.id}:remove")
43 | unschedule()
44 | removeChildDevices(getChildDevices())
45 | }
46 |
47 | private removeChildDevices(delete) {
48 | delete.each {deleteChildDevice(it.deviceNetworkId)}
49 | }
50 |
51 | def displayVersion() {
52 | setVersion()
53 | setLibraryVersion()
54 | section() {
55 | def currentYear = new Date().format("yyyy", location.timeZone)
56 | if(state.appType == "parent") { href "removePage", title:"${getImage("optionsRed")} Remove App and all child apps", description:"" }
57 | paragraph getFormat("line")
58 | if(state.version) {
59 | bMes = "
${state.name} - ${state.version}"
60 | } else {
61 | bMes = "
${state.name}"
62 | }
63 | bMes += "
Library Ver: ${state.libraryVersion}"
64 | bMes += "
"
65 | paragraph "${bMes}"
66 | paragraph("
Please consider making a small donation to support the developers application via PayPal™." +
67 | "
Copyright \u00a9 2018-${currentYear} SandersSoft™ Inc - All rights reserved.")
68 | }
69 | }
70 |
71 | void updateMyLabel(key=null) {
72 | def timeStamp = new Date().format("h:mm:ss a", location.timeZone)
73 |
74 | String myLabel = state.weatherStationName
75 | if ((myLabel == null) || !app.label.startsWith(myLabel)) {
76 | myLabel = app.label
77 | }
78 | if (myLabel.contains('
Unauthorized at ${timeStamp} "
84 | break;
85 | case 'updated':
86 | newLabel = myLabel + "
Updated at ${timeStamp} "
87 | break;
88 | case 'refreshing':
89 | newLabel = myLabel + "
Refreshing at ${timeStamp}"
90 | break;
91 | case 'retry':
92 | newLabel = myLabel + "
Re-trying #${state.retry} at ${timeStamp}"
93 | break;
94 | default:
95 | newLabel = myLabel
96 | break;
97 | }
98 | if (newLabel && (app.label != newLabel)) app.updateLabel(newLabel)
99 | }
100 |
101 | public String convertToCurrentTimeZone(String dateStr) {
102 |
103 | TimeZone utc = TimeZone.getTimeZone("UTC");
104 | SimpleDateFormat sourceFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
105 | SimpleDateFormat destFormat = new SimpleDateFormat('EEE MMM d, h:mm a');
106 | sourceFormat.setTimeZone(utc);
107 | Date convertedDate = sourceFormat.parse(dateStr);
108 | return destFormat.format(convertedDate);
109 |
110 | }
111 | //get the current time zone
112 |
113 | public String getCurrentTimeZone(){
114 | TimeZone tz = Calendar.getInstance().getTimeZone();
115 | return tz.getID();
116 | }
117 |
118 | String fmtTitle(String str) {
119 | return "
${str}"
120 | }
121 | String fmtDesc(String str) {
122 | return "
${str}
"
123 | }
124 | String fmtHelpInfo(String str) {
125 | String info = "${PARENT_DEVICE_NAME} v${VERSION}"
126 | String prefLink = "
${str}
${info}
"
127 | String topStyle = "style='font-size: 18px; padding: 1px 12px; border: 2px solid Crimson; border-radius: 6px;'" //SlateGray
128 | String topLink = "
${str}
${info}
"
129 | return "
"
130 | }
131 |
132 | String fmtDataFieldsWikiLink() {
133 | String str = 'Data Fields Wiki'
134 | String info = "${PARENT_DEVICE_NAME} v${VERSION}"
135 | String prefLink = "
${str}
${info}
"
136 | String topStyle = "style='font-size: 18px; padding: 1px 12px; border: 2px solid Crimson; border-radius: 6px;'" //SlateGray
137 | String topLink = "
${str}
${info}
"
138 | return "
"
139 | }
140 |
141 | def getImage(type) {
142 | def loc = "

"
144 | if(type == "checkMarkGreen") return "${loc}checkMarkGreen2.png height=30 width=30>"
145 | if(type == "optionsGreen") return "${loc}options-green.png height=30 width=30>"
146 | if(type == "optionsRed") return "${loc}options-red.png height=30 width=30>"
147 | if(type == "instructions") return "${loc}instructions.png height=30 width=30>"
148 | if(type == "logo") return "${loc}logo.png height=40>"
149 | if(type == "qmark") return "${loc}question-mark-icon.png height=16>"
150 | if(type == "qmark2") return "${loc}question-mark-icon-2.jpg height=16>"
151 | if(type == "button-red") return "${loc}/button-red.png height=30 width=30>"
152 | if(type == "qmark") return "${loc}question-mark-icon.png height=16>"
153 | if(type == "qmark2") return "${loc}question-mark-icon-2.jpg height=16>"
154 | }
155 |
156 | def getFormat(type, myText="") {
157 | if(type == "header-blue") return "
${myText}
"
158 | if(type == "header-red") return "
${myText}
"
159 | if(type == "line") return "
"
160 | if(type == "title") return "
${myText}
"
161 | if(type == "text-green") return "
${myText}
"
162 | if(type == "text-red") return "
${myText}
"
163 | if(type == "text-blue") return "
${myText}
"
164 | if(type == "button-blue") return "
${myText}"
165 | }
166 |
167 | def help() {
168 | section("${getImage('instructions')}
${app.name} Online Documentation", hideable: true, hidden: true) {
169 | paragraph "
Click this link to view Online Documentation for ${app.name}
"
170 | }
171 | }
172 |
173 | def sectionHeader(title){
174 | return getFormat("header-blue", "${getImage("Blank")}"+" ${title}")
175 | }
176 |
177 | def syncLogLevelApp2Children(level, time) {
178 | device.updateSetting("logLevel" ,[value: "${level}", type:"enum"])
179 | device.updateSetting("logLevelTime",[value: "${time}" , type:"enum"])
180 | checkLogLevel([level:level, time:time])
181 | }
182 |
183 | //Logging Level Options
184 | @Field static final Map LOG_LEVELS = [0:"Off", 1:"Error", 2:"Warn", 3:"Info", 4:"Debug", 5:"Trace"]
185 | @Field static final Map LOG_TIMES = [0:"Indefinitely", 1:"1 Minute", 5:"5 Minutes", 10:"10 Minutes", 15:"15 Minutes", 30:"30 Minutes", 60:"1 Hour", 120:"2 Hours", 180:"3 Hours", 360:"6 Hours", 720:"12 Hours", 1440:"24 Hours"]
186 | @Field static final String LOG_DEFAULT_LEVEL = 0
187 |
188 | //Call this function from within updated() and configure() with no parameters: checkLogLevel()
189 | void checkLogLevel(Map levelInfo = [level:null, time:null]) {
190 | unschedule(logsOff)
191 | //Set Defaults
192 | if (app) {
193 | if (settings.logLevel == null) app.updateSetting("logLevel",[value:LOG_DEFAULT_LEVEL, type:"enum"])
194 | if (settings.logLevelTime == null) app.updateSetting("logLevelTime",[value:"0", type:"enum"])
195 | } else {
196 | if (settings.logLevel == null) device.updateSetting("logLevel",[value:LOG_DEFAULT_LEVEL, type:"enum"])
197 | if (settings.logLevelTime == null) device.updateSetting("logLevelTime",[value:"0", type:"enum"])
198 | }
199 | //Schedule turn off and log as needed
200 | if (levelInfo.level == null) levelInfo = getLogLevelInfo()
201 | String logMsg = "Logging Level is: ${LOG_LEVELS[levelInfo.level]}"
202 | if (levelInfo.level >= 1 && levelInfo.time > 0) {
203 | logMsg += " for ${LOG_TIMES[levelInfo.time]}"
204 | runIn(60*levelInfo.time, logsOff, [overwrite: true])
205 | }
206 | if (levelInfo.time == 0) logMsg += " (${LOG_TIMES[levelInfo.time]})"
207 | logInfo(logMsg)
208 | }
209 |
210 | //Function for optional command
211 | void setLogLevel(String levelName, String timeName=null) {
212 | Integer level = LOG_LEVELS.find{ levelName.equalsIgnoreCase(it.value) }.key
213 | Integer time = LOG_TIMES.find{ timeName.equalsIgnoreCase(it.value) }.key
214 | if (app) {
215 | app.updateSetting("logLevel",[value:"${level}", type:"enum"])
216 | app.updateSetting("logLevelTime",[value:"${level}", type:"enum"])
217 | } else {
218 | device.updateSetting("logLevel",[value:"${level}", type:"enum"])
219 | device.updateSetting("logLevelTime",[value:"${time}", type:"enum"])
220 | }
221 | checkLogLevel(level: level, time: time)
222 | }
223 |
224 | Map getLogLevelInfo() {
225 | Integer level = settings.logLevel as Integer ?: 0
226 | Integer time = settings.logLevelTime as Integer ?: 0
227 | return [level: level, time: time]
228 | }
229 |
230 | void logsOff() {
231 | logInfo "Logging auto disabled"
232 | setLogLevel("Off","Indefinitely")
233 | }
234 |
235 | //Logging Functions
236 | def logMessage(String msg) {
237 | if (app) {
238 | return "
${app.name}: ${msg}"
239 | } else {
240 | return "
${device.name}: ${msg}"
241 | }
242 | }
243 |
244 | void logErr(String msg) {
245 | if (logLevelInfo.level>=1) log.error "${logMessage(msg)}"
246 | }
247 | void logWarn(String msg) {
248 | if (logLevelInfo.level>=2) log.warn "${logMessage(msg)}"
249 | }
250 | void logInfo(String msg) {
251 | if (logLevelInfo.level>=3) log.info "${logMessage(msg)}"
252 | }
253 |
254 | void logDebug(String msg) {
255 | if (logLevelInfo.level>=4) log.debug "${logMessage(msg)}"
256 | }
257 |
258 | void logTrace(String msg) {
259 | if (logLevelInfo.level>=5) log.trace "${logMessage(msg)}"
260 | }
--------------------------------------------------------------------------------
/hubitat/smartapps/kurtsanders/ambient-weather-station.src/ambient-weather-station.groovy:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2025 SanderSoft™
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at:
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing permissions and limitations under the License.
12 | *
13 | * Ambient Weather Station
14 | *
15 | * Author: Kurt Sanders, SanderSoft™
16 | *
17 | * Dates: 2018,2019,2020,2021,2022,2023,2024,2025
18 | */
19 |
20 | #include kurtsanders.AWSLibrary
21 | @Field static String PARENT_DEVICE_NAME = "Ambient Weather Station"
22 | @Field static final String VERSION = "6.7.3"
23 |
24 | //************************************ Version Specific ***********************************
25 | String appModified() { return "Oct-24-2025" }
26 | //*************************************** Constants ***************************************
27 |
28 | String appNameVersion() { return "Ambient Weather Station " + VERSION }
29 | String appShortName() { return "STAmbientWeather " + VERSION }
30 |
31 | String DTHName() { return PARENT_DEVICE_NAME }
32 | String DTHRemoteSensorName() { return PARENT_DEVICE_NAME + " Remote Sensor"}
33 | String DTHPMSensorName() { return "Ambient Particulate Monitor"}
34 | String DTHDNI() { return "${app.id}:MyAmbientWeatherStation" }
35 | String DTHDNIRemoteSensorName() { return "${app.id}:MyAmbientRemoteSensor"}
36 | String DTHDNIPMName() { return "${app.id}:MyAmbientParticulateMonitor"}
37 | String DTHDNIActionTiles() { return "${app.id}:MyAmbientSmartWeatherStationTile" }
38 | Integer MaxNumRemoteSensors() { return 8 }
39 |
40 | String DTHnamespace() { return NAMESPACE }
41 | String appAuthor() { return AUTHOR_NAME }
42 | String AppImg(imgName) { return GITHUB_IMAGES_LINK + "${imgName}" }
43 | String wikiURL(pageName) { return "https://github.com/KurtSanders/STAmbientWeather/wiki/$pageName"}
44 | Integer wm2lux(value) { return (value * 126.7).toInteger() }
45 | Integer wm2fc(value) { return (wm2lux(value) * 0.0929).toInteger() }
46 |
47 | // ============================================================================================================
48 | // This APP key is ONLY for this application - Do not copy or use elsewhere
49 | @Field static final String APPKEY = "33054086b3d745779f5ac35e147baa76f13e75d44ea245388ba598911905fb50"
50 | // ============================================================================================================
51 |
52 | definition(
53 | name : PARENT_DEVICE_NAME,
54 | namespace : NAMESPACE,
55 | author : AUTHOR_NAME,
56 | description : "Integrate your Ambient™ Weather Station and remote weather/soil/particle monitor sensors to Hubitat™",
57 | category : "",
58 | iconUrl : "",
59 | iconX2Url : "",
60 | documentationLink : COMM_LINK,
61 | singleInstance : false,
62 | pausable : false
63 | )
64 | preferences {
65 | page(name: "apiPage")
66 | page(name: "mainPage")
67 | page(name: "optionsPage")
68 | page(name: "unitsPage")
69 | page(name: "remoteSensorPage")
70 | page(name: "notifyPage")
71 | page(name: "DataPage")
72 | }
73 |
74 | def apiPage() {
75 | if (apiKey == null) {
76 | log.info "${app.name}: First Run: Initializing default 'Units of Measure' values to Imperial system"
77 | app.updateSetting("tempUnits", [type: "enum", value: "°F"])
78 | app.updateSetting("windUnits", [type: "enum", value: "mph"])
79 | app.updateSetting("measureUnits", [type: "enum", value: "in"])
80 | app.updateSetting("baroUnits", [type: "enum", value: "inHg"])
81 | app.updateSetting("solarRadiationTileDisplayUnits", [type: "enum", value: "W/m²"])
82 | }
83 | if (keepDataKeysAllOption == null) {
84 | log.info "${app.name}: First Run: Initializing default keepDataKeysAllOption = True"
85 | app.updateSetting("keepDataKeysAllOption", [type: "bool", value: true])
86 | }
87 |
88 | dynamicPage(name: "apiPage", submitOnChange: true, nextPage: "mainPage", uninstall: true, install: false ) {
89 | section(sectionHeader("Ambient Weather Station API Key")) {
90 | input ( name: "apiKey", type: "text",
91 | title: fmtTitle("Enter your Ambient Weather Station API Key below (Required)"),
92 | required: true
93 | )
94 | paragraph ""
95 | href(name: "APIKeyLink",
96 | title: fmtTitle("Here is a WebLink to display your Ambient Weather Station Account Page with API key (scroll to the bottom of the page to copy your API string)"),
97 | required: false,
98 | style: "external",
99 | url: "https://ambientweather.net/account",
100 | description: fmtTitle("tap to view your weather station account page and copy your API key")
101 | )
102 | }
103 | }
104 | }
105 |
106 | def mainPage() {
107 | def apiappSetupCompleteBool = false
108 | if (apiKey == state.apiKey && state.ambientMap) {
109 | apiappSetupCompleteBool = true
110 | } else {
111 | state.apiKey = apiKey
112 | apiappSetupCompleteBool = AmbientStationData(0)
113 | }
114 | def setupMessage = ""
115 | def setupTitle = "${appNameVersion()} API Settings Check"
116 | def nextPageName = getAllChildDevices().count{it}>0?"optionsPage":"remoteSensorPage"
117 | state.retry = 0
118 | def AmbientStationDataRC = (state.ambientMap)?true:false
119 | if (!state.ambientMap) {
120 | AmbientStationDataRC = AmbientStationData(0)
121 | }
122 | if (apiappSetupCompleteBool && AmbientStationDataRC) {
123 | setupMessage = "SUCCESS! You have completed entering a valid Ambient API Key for ${appNameVersion()}. "
124 | setupMessage += (weatherStationMac)?"Please Press 'Next' for additional configuration choices.":"I found ${state.ambientMap.size()} reporting weather station(s)."
125 | setupTitle = "
Please confirm the Ambient Weather Station Information below and if correct, Tap 'NEXT' to continue to the 'Settings' page'"
126 | } else {
127 | setupMessage = "
Ambient API Setup INCOMPLETE or MISSING!\n\nPlease check and/or complete the REQUIRED Ambient Weather API key setup for ${appNameVersion()}.\n\nAPI Error message: ${state.httpError}"
128 | nextPageName = null
129 | }
130 | dynamicPage(name: "mainPage", title: setupTitle, submitOnChange: true, nextPage: nextPageName, uninstall:true, install:false) {
131 | section(hideable: apiappSetupCompleteBool, hidden: apiappSetupCompleteBool, sectionHeader(setupMessage) ) {
132 | paragraph "
The API string key is used to securely connect your weather station to ${appNameVersion()}."
133 | paragraph image: AppImg("blue-ball-100.jpg"),
134 | title: "
Required API Key",
135 | required: false,
136 | informationList("apiHelp")
137 | href(name: "hrefReadme",
138 | title: fmtTitle("${appNameVersion()} Setup/Read Me Page"),
139 | required: false,
140 | style: "external",
141 | url: "https://github.com/KurtSanders/STAmbientWeather#hubitat-installation",
142 | description: "tap to view the Setup/Read Me page")
143 | href(name: "hrefAmbient",
144 | title: fmtTitle("Ambient Weather Dashboard Account Page for API Key"),
145 | required: false,
146 | style: "external",
147 | url: "https://dashboard.ambientweather.net/account",
148 | description: "tap to login and view your Ambient Weather's dashboard")
149 | }
150 | if (apiappSetupCompleteBool && AmbientStationDataRC) {
151 | if (weatherStationMac) {
152 | setStateWeatherStationData()
153 | state.weatherStationMac = weatherStationMac
154 | countRemoteTempHumiditySensors()
155 | section (sectionHeader("Ambient Weather Station Information")) {
156 | paragraph image: AppImg("blue-ball-100.jpg"),
157 | title: fmtTitle("${state.weatherStationName}"),
158 | required: false,
159 | "Name: ${state.weatherStationName}" +
160 | "\nLocation: ${state.weatherStationLocation?:'Not Provided'}" +
161 | "\nMac Address: ${state.ambientMap[state.weatherStationDataIndex].macAddress}" +
162 | "\nRemote Temp/Hydro/Moisture Sensors: ${state.countRemoteTempHumiditySensors}" +
163 | "\nAQIN Particulate Monitor Sensor: ${state.countParticulateMonitors}"
164 | href(name: "
Weather Station Options",
165 | page: nextPageName,
166 | description: "
")
167 | }
168 |
169 | } else {
170 | def weatherStationList = [:]
171 | def stationlocation
172 | state.ambientMap.each {
173 | stationlocation = it.info.location?:it.info.containsKey("coords")?it.info.coords.location:''
174 | weatherStationList << [[ "${it.macAddress}" : "${it.info.name}${stationlocation?' @ ':''}${stationlocation}" ]]
175 | }
176 | section ("Ambient Weather Station Information") {
177 | input (name: "weatherStationMac", submitOnChange: true, type: "enum",
178 | title: fmtTitle("Select the Weather Station to Install"),
179 | options: weatherStationList,
180 | multiple: false,
181 | required: true
182 | )
183 | }
184 | }
185 | }
186 | section ("STAmbientWeather™ - ${appAuthor()}") {
187 | href(name: "hrefVersions",
188 | image: AppImg("readme.png"),
189 | title: fmtTitle("Release Notes for ${VERSION} : ${appModified()}"),
190 | required: false,
191 | style:"embedded",
192 | url: wikiURL("Features-by-Version")
193 | )
194 | }
195 | }
196 | }
197 |
198 | def unitsPage() {
199 | dynamicPage(name: "unitsPage", title: "Ambient Units of Measure Settings for: '${state.weatherStationName}'",
200 | uninstall : false,
201 | install : false ) {
202 | section("Weather Station Unit of Measure Options") {
203 | input ( name: "tempUnits", type: "enum",
204 | title: fmtTitle("Select Temperature Units of Measure"),
205 | options: ['°F':'Fahrenheit °F','°C':'Celsius °C'],
206 | defaultValue: "°F",
207 | required: true
208 | )
209 | input ( name: "windUnits", type: "enum",
210 | title: fmtTitle("Select Wind Speed Units of Measure"),
211 | options: ['mph':'Miles per Hour','fps':'Feet per Second','mps':'Meter per Second','kph':'Kilometers per Hour','knotts':'Knotts'],
212 | defaultValue: "mph",
213 | required: true
214 | )
215 | input ( name: "measureUnits", type: "enum",
216 | title: fmtTitle("Select Rainfall Units of Measure"),
217 | options: ['in':'Inches','mm':'Millimeters','cm':'Centimeters'],
218 | defaultValue: "in",
219 | required: true
220 | )
221 | input ( name: "baroUnits", type: "enum",
222 | title: fmtTitle("Select Barometer Units of Measure"),
223 | options: ['inHg':'inHg','mmHg':'mmHg', 'hPa':'hPa'],
224 | defaultValue: "inHg",
225 | required: true
226 | )
227 | input ( name: "solarRadiationTileDisplayUnits", type: "enum",
228 | title: fmtTitle("Select Solar Radiation ('Light') Units of Measure"),
229 | options: ['W/m²':'Imperial Units (W/m²)','lux':'Metric Units (lux)', 'fc':'Foot Candles (fc)'],
230 | defaultValue: "W/m²",
231 | required: true
232 | )
233 | input ( name: "solarRadiationDecimalFormat", type: "enum",
234 | title: fmtTitle("Select Solar Radiation ('Light') Decimal Format"),
235 | options: [0,1,2],
236 | defaultValue: 0,
237 | required: true
238 | )
239 | }
240 | }
241 | }
242 |
243 | def DataPage() {
244 | dynamicPage(name: "DataPage", title: getFormat("title", myText="Ambient Weather Station Data Key Import Selector"), submitOnChange: true,
245 | uninstall : false,
246 | install : false ) {
247 | section() {
248 | input ( name: "keepDataKeysAllOption", type: "bool",
249 | title: "
Select ALL Data Keys?' Toggle OFF to ONLY import partial weather data keys into Hubitat for '${state.weatherStationName} Weather Station'",
250 | required: true,
251 | submitOnChange: true,
252 | defaultValue:true
253 | )
254 | }
255 | if(!keepDataKeysAllOption) {
256 | section () {
257 | def dateKeystoKeep = ["date","tz","dateutc"]
258 | def dataValues = new JsonSlurper().parseText(JsonOutput.toJson(state.respdata))
259 | dataValues = dataValues[state.weatherStationDataIndex].lastData.keySet()
260 | dataValues.removeAll(dateKeystoKeep)
261 | section (getFormat("header-blue","Partial Data Keys Selector")) {
262 | def wikiURL = "https://github.com/ambient-weather/api-docs/wiki/Device-Data-Specs"
263 | paragraph("
This Wiki lists all the data key parameters that a AMbient Weather Station device might send. Note: Not all devices send all the data key parameters.\n\n
Weather Data Name Wiki Documentation Wiki (opens in a new browser tab/window, close tab/window to return): Click Here")
264 | input ( name: "keepDataKeys", type: "enum",
265 | multiple: true,
266 | title: "
Select the Data Keys of your weather station to ONLY import into Hubitat for '${state.weatherStationName}.'",
267 | options: dataValues.sort(),
268 | offerAll: false,
269 | submitOnChange: true,
270 | required: true
271 | )
272 | }
273 | }
274 | }
275 | selectWeatherKeys(state.respdata)
276 | }
277 | }
278 |
279 | def selectWeatherKeys(respdata) {
280 | // Keep these time/date related data keys in the filtered dataset
281 | def dateKeystoKeep = ["date","tz","dateutc"]
282 | logDebug "==> Start state.ambientMap= ${state.ambientMap}"
283 |
284 | if (keepDataKeysAllOption) {
285 | logDebug "Keeping ALL Data Keys..."
286 | state.ambientMap = respdata
287 | } else if (keepDataKeys) {
288 | if (debugVerbose) {
289 | logDebug "Partial Data Keys Selected"
290 | logDebug "Keeping only '${keepDataKeys}' keys"
291 | }
292 | keepDataKeys.addAll(dateKeystoKeep) // Add back the date related data keys to the keepDataKeys array
293 | AWSmap = new JsonSlurper().parseText(JsonOutput.toJson(state.respdata))
294 | AWSmap[state.weatherStationDataIndex].lastData.keySet().retainAll(keepDataKeys)
295 | state.ambientMap = AWSmap
296 | if (debugVerbose) {
297 | logDebug "==> End state.ambientMap keys = ${state.ambientMap[state.weatherStationDataIndex].lastData.size()}"
298 | }
299 | } else {
300 | logWarn "keepDataKeys LIST is Null => ${keepDataKeys}.. Changing 'keepDataKeysAllOption' to 'True' in AWS Preferences"
301 | app.updateSetting("keepDataKeysAllOption", true)
302 | state.ambientMap = respdata
303 | }
304 | if (debugVerbose) {
305 | logDebug "==> End state.ambientMap= ${state.ambientMap}"
306 | logDebug "==> End state.ambientMap keys = ${state.ambientMap[state.weatherStationDataIndex].lastData.size()}"
307 | }
308 | }
309 |
310 | def optionsPage () {
311 | logInfo "Ambient Weather Station: Mac: ${weatherStationMac}, Name/Loc: ${state.weatherStationName}/${state.weatherStationLocation}"
312 | if (app.label.contains('
WebLink to ${app.name}")
318 | paragraph ("")
319 | }
320 | section(sectionHeader("Ambient Weather Station (AWS) Preference Settings for: '${state.weatherStationName}'")) {
321 | input ( name: "schedulerFreq", type: "enum",
322 | title: fmtTitle("Poll the Ambient Weather Station Server every"),
323 | options: POLLING_OPTIONS_MAP,
324 | required: true,
325 | defaultValue: '15 mins'
326 | )
327 | input (name: "showBattery", type: "bool",
328 | title: fmtTitle("Show battery level from sensor(s)? (Ambient devices only report 0% and 100%)"),
329 | defaultValue: true,
330 | required: true
331 | )
332 | href(name: "Weather Units of Measure",
333 | title: fmtTitle("Select Weather Units of Measure"),
334 | required: false,
335 | defaultValue: "Tap to Select Units",
336 | description: tempUnits?"${unitsSet()}":"Tap to Select Units",
337 | page: "unitsPage")
338 | href(name: "Weather Station Filtered Data Key Import Selector",
339 | title: fmtTitle("Select/DeSelect the weather station data keys to import (Required)"),
340 | required: true,
341 | defaultValue: "Tap to Select Partial Data Keys to Import",
342 | description: keepDataKeysAllOption?"All ${state?.ambientMap[state.weatherStationDataIndex].lastData.size()-3} Weather Data Keys are selected. Tap to Select Partial Weather Data Keys":"${keepDataKeys?.size()} of ${state.respdata[state?.weatherStationDataIndex].lastData.size()-3} Data Keys Selected. Tap to Modify Selection or Select ALL Weather Data Keys",
343 | page: "DataPage")
344 | href(name: "Activate Weather Alerts/Notification",
345 | title: fmtTitle("Weather Alerts/Notification (Optional)"),
346 | required: false,
347 | defaultValue: (checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled]))?"Alerts Activated ":"Tap to Activate Alerts",
348 | description: (checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled]))?"Alerts Activated ":"Tap to Activate Alerts",
349 | page: "notifyPage")
350 | href(name: "Weather Sensor Device Label Management",
351 | title: fmtTitle("Weather Sensor Names"),
352 | description: "Tap to Change the Device Label of ${state.countRemoteTempHumiditySensors + state.countParticulateMonitors + 1} Remote Weather Sensors",
353 | required: false,
354 | page: "remoteSensorPage")
355 | }
356 | section (sectionHeader('Name this instance of Ambient Weather Station')) {
357 | label ( name: "name",
358 | title: fmtTitle("Assign a name to this SmartApp"),
359 | state: (name ? "complete" : null),
360 | defaultValue: state.weatherStationName,
361 | required: false,
362 | submitOnChange: true
363 | )
364 | }
365 | section(sectionHeader("AWS Logging Options")) {
366 | if (logLevel == null && logLevelTime == null) {
367 | log.info "${app.name}: Setting Innital logLevel and LogLevelTime defaults"
368 | app.updateSetting(logLevel, [type: "enum", value: [5]])
369 | app.updateSetting(logLevelTime, [type: "enum", value: [30]])
370 | }
371 | //Logging Options
372 | input name: "logLevel", type: "enum", title: fmtTitle("Logging Level"), submitOnChange: true,
373 | description: fmtDesc("Logs selected level and above"), defaultValue: 3, options: LOG_LEVELS
374 | input name: "logLevelTime", type: "enum", title: fmtTitle("Logging Level Time"), submitOnChange: true,
375 | description: fmtDesc("Time to enable Debug/Trace logging"),defaultValue: 10, options: LOG_TIMES
376 | input name: "SyncLogOptions", type: "button", title: "Sync Log Options in all Devices"
377 | }
378 | }
379 | }
380 |
381 | def remoteSensorPage() {
382 | getAllChildDevices().each {
383 | logDebug "Device: ${it.deviceNetworkId} = ${it.label}"
384 | app.updateSetting(it.deviceNetworkId, [type: "string", value: it.label])
385 | }
386 |
387 | dynamicPage(name: "remoteSensorPage", nextPage: "optionsPage", uninstall:false, install : false ) {
388 | if (state.ambientMap[state.weatherStationDataIndex].lastData?.tempinf) {
389 | section (sectionHeader("Enter a location name for your AWS Weather console located inside the house?")) {
390 | input (name: "${DTHDNIRemoteSensorName()}0",
391 | type: "text",
392 | title: "",
393 | required: true,
394 | defaultValue: 'Kitchen',
395 | submitOnChange: true
396 | )
397 | }
398 | }
399 | def i = 1
400 | def lastData = state.ambientMap[state.weatherStationDataIndex].lastData
401 | if ( state?.countRemoteTempHumiditySensors > 0) {
402 | section(sectionHeader("Provide Location names for your ${state?.countRemoteTempHumiditySensors} remote temperature/hydro sensors")) {
403 | paragraph getFormat("text-red",
404 | "You MUST create short descriptive names for each remote sensor or accept the default provided. Do not use ANY special characters in the device names.\n\n" +
405 | "Please note that remote sensors are numbered based in the bit switch on the Ambient Weather sensor (1-8) and reported on Ambient Network API as 'tempNf' or 'soiltempN' where N is an integer 1-8. " +
406 | "If a remote weather/soil sensor is deleted from your AWS network or non responsive from your group of Ambient remote sensors, you may have to re-verify and/or rename the remainder of the remote sensors in this app and manually delete that device sensor from the Hubitat 'Devices' page.")
407 | for (i; i <= MaxNumRemoteSensors(); i++) {
408 | if (lastData["temp${i}f"]) {
409 | input (
410 | name: "${DTHDNIRemoteSensorName()}${i}",
411 | type: "text",
412 | title: getImage("checkMarkGreen") + fmtTitle("Ambient Weather Station Remote Device #${i} → Temp Sensor (Current: ${lastData["temp${i}f"]}°)"),
413 | defaultValue: "Temp Sensor #${i}",
414 | required: true
415 | )
416 | }
417 | if (lastData["soiltemp${i}"]) {
418 | input (
419 | name: "${DTHDNIRemoteSensorName()}${i}",
420 | type: "text",
421 | title: getImage("checkMarkGreen") + fmtTitle("Ambient Weather Station Remote Device #${i} → Soil Temp Sensor (Current: ${lastData["soiltemp${i}"]}°)"),
422 | defaultValue: "Soil Temp Sensor #${i}",
423 | required: true
424 | )
425 | }
426 | if (lastData["soilhum${i}"]) {
427 | input (
428 | name: "${DTHDNIRemoteSensorName()}${i}",
429 | type: "text",
430 | title: getImage("checkMarkGreen") + fmtTitle("Ambient Weather Station Remote Device #${i} → Soil Moisture Sensor (Current: ${lastData["soilhum${i}"]})"),
431 | defaultValue: "Soil Moisture Sensor #${i}",
432 | required: true
433 | )
434 | }
435 | }
436 | }
437 | }
438 | if (lastData.findAll { it.key.startsWith("pm") }.size() > 0) {
439 | section() {
440 | paragraph "Ambient Weather Station Particulate Monitor Location Name"
441 | paragraph image: AppImg("ambient-weather-pm25.jpg"),
442 | title: fmtTitle("Provide a friendly short name for your Ambient Particulate Monitor PM10/PM25/AQIN/CO2"),
443 | required: false,
444 | null
445 | input (
446 | name: "${DTHDNIPMName()}",
447 | type: "text",
448 | title: fmtTitle("Ambient Weather Station Particulate Monitor PM10/PM25/AQIN/CO2"),
449 | defaultValue: "AWS Particle Monitor",
450 | required: true
451 | )
452 | }
453 | }
454 | }
455 | }
456 |
457 | def notifyPage() {
458 | dynamicPage(name: "notifyPage", title: "Weather Alerts/Notifications", uninstall: false, install: false) {
459 | section(sectionHeader("Enable Pushover™ and/or Twilio™ service(s). (Must install virtual device(s) and have an active service account):")) {
460 | input ("pushoverEnabled", "bool", title: "Use Pushover™ and/or Twilio™ Service(s) for Alert Notifications", required: false, submitOnChange: true)
461 | if (pushoverEnabled) {
462 | input(name: "pushoverDevices", type: "capability.notification", title: "", required: false, multiple: true,
463 | description: "Select notification device(s)", submitOnChange: true)
464 | paragraph ""
465 | }
466 | }
467 |
468 | if (checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled])) {
469 | section (sectionHeader("Weather Station Notify Options")) {
470 | input ( name: "notifyAlertFreq", type: "enum",
471 | required: checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled]),
472 | title: "Restrict notification(s) per event type to once every NUMBER of hours (Default is 24, Once/day)",
473 | options: [0,1,2,4,6,12,24],
474 | defaultValue: 24,
475 | submitOnChange: true,
476 | multiple: false
477 | )
478 | if (state.ambientMap[state.weatherStationDataIndex].lastData.keySet().grep(~/^temp1?f$/)) {
479 | input ( name: "notifyAlertLowTemp", type: "number", required: false,
480 | title: "Notify when a temperature value is EQUAL OR BELOW this value. Leave field blank to cancel notification."
481 | )
482 | input ( name: "notifyAlertHighTemp", type: "number", required: false,
483 | title: "Notify when a temperature value is EQUAL OR ABOVE this value. Leave field blank to cancel notification."
484 | )
485 | }
486 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('hourlyrainin')) {
487 | input ( name: "notifyRain", type: "bool", required: false,
488 | title: "Notify when RAIN is detected"
489 | )
490 | }
491 | }
492 | }
493 | section (hideable: true, hidden: true, sectionHeader("Last Notification Times")) {
494 | paragraph image: "",
495 | required: false,
496 | SMSNotifcationHistory()
497 | }
498 | }
499 | }
500 |
501 | private static boolean checkRequired(vars) {
502 | def rc = false
503 | vars.each {
504 | if ((it) || it==true) {
505 | rc = true
506 | }
507 | }
508 | return rc
509 | }
510 |
511 | def appButtonHandler(String buttonName) {
512 | logDebug "appButtonHandler: buttonName: ${buttonName}"
513 | if (buttonName == "SyncLogOptions") {
514 | logDebug "logLevel: ${settings.logLevel} logLevelTime: ${settings.logLevelTime}"
515 | Integer level = settings.logLevel as Integer ?: 0
516 | Integer time = settings.logLevelTime as Integer ?: 0
517 | logInfo "level: ${LOG_LEVELS[level]} and time: ${LOG_TIMES[time]}"
518 | logInfo "${app.name}: Current LogLevel is ${getLogLevelInfo()}"
519 | logInfo "Synchronizing all AWS devices to 'level: ${LOG_LEVELS[level]}' and time: '${LOG_TIMES[time]}'"
520 | getAllChildDevices().each {
521 | logInfo "Before Sync → Current LogLevel of '${it.label}' is ${it.getLogLevelInfo()}"
522 | it.syncLogLevelApp2Children(level, time)
523 | logInfo "After Sync → Current LogLevel of '${it.label}' is ${it.getLogLevelInfo()}"
524 | }
525 | }
526 | }
527 |
528 | def initialize() {
529 | logInfo "initialize()"
530 | def now = now()
531 | // Initialize/Reset Alert Warnings DateTime values
532 | state.notifyAlertLowTempDT = 0
533 | state.notifyAlertHighTempDT = 0
534 | state.notifyRainDT = 0
535 | state.notifySevereAlertDT = 0
536 | state.notifyAlertFreq = notifyAlertFreq?:24
537 | state.tempUnitsDisplay = tempUnits
538 | state.windUnitsDisplay = windUnits
539 | state.measureUnitsDisplay = measureUnits
540 | state.baroUnitsDisplay = baroUnits
541 |
542 | // Check for all devices needed to run this app
543 | addAmbientChildDevice()
544 | // Set user defined refresh rate
545 | if(state.schedulerFreq!=schedulerFreq) {
546 | logDebug "state.schedulerFreq → ${state.schedulerFreq} and schedulerFreq → ${schedulerFreq}"
547 | def d = getChildDevice(state.deviceId)
548 | logInfo "Updating your Cron REFRESH schedule from ${state.schedulerFreq?:0} mins to ${schedulerFreq} mins and updating ${d} AWS 'scheduleFreqMin' device attribute"
549 | setScheduler(schedulerFreq)
550 | d.sendEvent(name: "scheduleFreqMin", value: schedulerFreq)
551 | state.schedulerFreq = schedulerFreq
552 | }
553 | checkLogLevel()
554 | }
555 |
556 | def installed() {
557 | logInfo "installed()"
558 | state.deviceId = DTHDNI()
559 | initialize()
560 | runIn(10, refresh)
561 | }
562 |
563 | def updated() {
564 | logInfo "updated()"
565 | initialize()
566 | getAllChildDevices().each {
567 | logInfo "Clearing current states of device '${it.label}' at '${it.deviceNetworkId}'"
568 | it.deleteDeviceData()
569 | }
570 | refresh()
571 | }
572 |
573 | def setPollingInterval(pollingInterval=null) {
574 | logDebug "==> setPollingInterval(${pollingInterval})"
575 | // Set user defined refresh rate
576 | if (POLLING_OPTIONS_MAP.containsKey(pollingInterval)) {
577 | app.updateSetting("schedulerFreq", [type: "enum", value: pollingInterval])
578 | if(state.schedulerFreq!=schedulerFreq) {
579 | logDebug "state.schedulerFreq → ${state.schedulerFreq} and schedulerFreq → ${schedulerFreq}"
580 | def d = getChildDevice(state.deviceId)
581 | logInfo "Updating your Cron REFRESH schedule from ${state.schedulerFreq?:0} mins to ${schedulerFreq} mins and updating ${d.name} device attribute"
582 | setScheduler(schedulerFreq)
583 | d.sendEvent(name: "scheduleFreqMin", value: schedulerFreq)
584 | state.schedulerFreq = schedulerFreq
585 | }
586 | } else {
587 | logErr "Invalid polling interval '${pollingInterval}'. Valid polling interval keys are ${POLLING_OPTIONS_MAP.keySet()}"
588 | return false
589 | }
590 | return true
591 | }
592 |
593 | def scheduleCheckReset(quiet=false) {
594 | if (schedulerFreq!='0'){
595 | setScheduler(schedulerFreq)
596 | if (!quiet) {
597 | Date start = new Date()
598 | Date end = new Date()
599 | use( TimeCategory ) {
600 | end = start + schedulerFreq.toInteger().minutes
601 | }
602 | logInfo "Reset the next CRON Refresh to ~${schedulerFreq} mins from now (${end.format("h:mm:ss a", location.timeZone)}) to avoid excessive HTTP requests"
603 | }
604 | }
605 | }
606 |
607 | def refresh() {
608 | updateMyLabel('refreshing')
609 | logInfo "Device: 'Refresh ALL'"
610 | def runID = new Random().nextInt(10000)
611 | main(runID)
612 | }
613 |
614 | def autoScheduleHandler() {
615 | def runID = new Random().nextInt(10000)
616 | logInfo "Executing Cron Schedule runID: ${runID} every ${schedulerFreq} min(s)"
617 | main(runID)
618 | }
619 |
620 | def main(runID=null) {
621 | runID = (runID)?:new Random().nextInt(10000)
622 | logInfo "Main(#${runID}) Section: Executing Ambient Weather Station API's for: '${state.weatherStationName}'"
623 |
624 | // Ambient Weather Station API
625 | ambientWeatherStation(runID)
626 |
627 | // Notify Events Check
628 | notifyEvents()
629 | updateMyLabel('updated')
630 | }
631 |
632 | def retryQuick(data) {
633 | logInfo "retryQuick #${state.retry} RunID: ${data.runID}"
634 | // Ambient Weather Station API
635 | updateMyLabel('retry')
636 | ambientWeatherStation(data.runID)
637 |
638 | // Notify Events Check
639 | notifyEvents()
640 | }
641 |
642 | def ambientWeatherStation(runID="missing runID") {
643 | // Ambient Weather Station
644 | logInfo "Executing full ambientWeatherStation routine runID: ${runID}"
645 | def d = getChildDevice(state.deviceId)
646 | def okTOSendEvent = true
647 | def remoteSensorDNI = ""
648 | def now = new Date().format('EEE MMM d, h:mm:ss a',location.timeZone)
649 | def nowTime = new Date().format('h:mm a',location.timeZone).toLowerCase()
650 | def currentDT = new Date()
651 | def sendEventOptions = ""
652 | if (AmbientStationData(runID)) {
653 | logDebug "httpget resp status = ${state.respStatus}"
654 | logInfo "Processing Ambient Weather data returned from AmbientStationData)"
655 | setStateWeatherStationData()
656 | convertStateWeatherStationData()
657 | if (settings.logLevel > 4) {
658 | state.ambientMap[state.weatherStationDataIndex].each{ k, v ->
659 | logTrace "${k} = ${v}"
660 | if (k instanceof Map) {
661 | k.each { x, y ->
662 | logTrace "${x} = ${y}"
663 | }
664 | }
665 | if (v instanceof Map) {
666 | v.each { x, y ->
667 | logTrace "${x} = ${y}"
668 | }
669 | }
670 | }
671 | }
672 | logDebug "Checking Weather Station data array for 'Last Rain Date' information..."
673 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('lastRain')) {
674 | logDebug "Weather Station has 'Last Rain Date' information...Processing"
675 | def dateRain = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", state.ambientMap[state.weatherStationDataIndex].lastData.lastRain)
676 | use (groovy.time.TimeCategory) {
677 | def lastRainDuration = ((currentDT - dateRain) =~ /(.+)\b,/)[0][1]
678 | logDebug ("lastRainDuration -> ${lastRainDuration}")
679 | if (lastRainDuration) {
680 | d.sendEvent(name:"lastRainDuration", value: lastRainDuration, displayed: false)
681 | }
682 | }
683 | }
684 | d.sendEvent(name:"scheduleFreqMin" , value: schedulerFreq, descriptionText: "AWS Polling Interval")
685 | d.sendEvent(name:"lastSTupdate" , value: tileLastUpdated())
686 | d.sendEvent(name:"macAddress" , value: state.ambientMap[state.weatherStationDataIndex].macAddress)
687 |
688 | // Update Main Weather Device with Remote Sensor 1 values if tempf does not exist, same with humidity
689 | if (!state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('tempf')) {
690 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('temp1f')) {
691 | d.sendEvent(name:"temperature", value: state.ambientMap[state.weatherStationDataIndex].lastData.temp1f, units: state.tempUnitsDisplay)
692 | }
693 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('humidity1')) {
694 | d.sendEvent(name:"humidity", value: state.ambientMap[state.weatherStationDataIndex].lastData.humidity1, units: "%", displayed: false)
695 | d.sendEvent(name:"humidity_display", value: "${state.ambientMap[state.weatherStationDataIndex].lastData.humidity1}%")
696 | }
697 | }
698 | // Update Main Weather Device with Remote Sensor 1 values if tempinf does not exist, same with humidityin
699 | if (!state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('tempinf')) {
700 | logDebug "Fixing Main Station for inside temp"
701 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('temp1f')) {
702 | d.sendEvent(name:"tempinf", value: state.ambientMap[state.weatherStationDataIndex].lastData.temp1f, units: state.tempUnitsDisplay, displayed: false)
703 | d.sendEvent(name:"tempinf_display", value: "${state.ambientMap[state.weatherStationDataIndex].lastData.temp1f}${state.tempUnitsDisplay}")
704 | }
705 | }
706 | if (!state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('humidityin')) {
707 | logDebug "Fixing Main Station for inside humidity"
708 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('humidity1')) {
709 | d.sendEvent(name:"humidityin", value: state.ambientMap[state.weatherStationDataIndex].lastData.humidity1, units: "%", displayed: false)
710 | d.sendEvent(name:"humidityin_display", value: "${state.ambientMap[state.weatherStationDataIndex].lastData.humidity1}%")
711 | }
712 | }
713 |
714 | state.ambientServerDate=convertToCurrentTimeZone(state.respdata[state.weatherStationDataIndex].lastData.date)
715 |
716 | // Send AWS Info metaata events
717 | def infoBase = state.ambientMap[state.weatherStationDataIndex].info
718 | d.sendEvent(name: 'pwsName', value: infoBase?.name)
719 | d.sendEvent(name: 'location', value: infoBase?.location)
720 | d.sendEvent(name: 'lat', value: infoBase?.coords.coords.lat)
721 | d.sendEvent(name: 'lon', value: infoBase?.coords.coords.lon)
722 | d.sendEvent(name: 'address', value: infoBase?.coords.address)
723 | d.sendEvent(name: 'elevation', value: infoBase?.coords.elevation)
724 |
725 | // Loop through the weather data elements creating events
726 | state.ambientMap[state.weatherStationDataIndex].lastData.each{ k, v ->
727 | logDebug "Received Data ${k} = ${v}"
728 |
729 | // Post weather data as a displayed string value
730 | switch(k) {
731 | case ~/.*rain.*/:
732 | d.sendEvent(name: "${k}_display", value: "${v} ${state.measureUnitsDisplay}")
733 | break
734 | case ~/^barom.*/:
735 | d.sendEvent(name: "${k}_display", value: "${v} ${state.baroUnitsDisplay}")
736 | break
737 | case ~/^tempi?n?f$|^dewPoint$|^feelsLikein$|^feelsLike$/:
738 | d.sendEvent(name: "${k}_display", value: "${v}${state.tempUnitsDisplay}")
739 | break
740 | case ~/^wind.*|^maxdailygust$/:
741 | d.sendEvent(name: "${k}_display", value: "${v} ${state.windUnitsDisplay}")
742 | break
743 | case ~/^humidity($|1|in)/:
744 | d.sendEvent(name: "${k}_display", value: "${v}%")
745 | break
746 | case ~/^batt.*/:
747 | // Change device battery level to 100% if the User preferences showBattery value has been defined and false
748 | if ( (showBattery != null) && (!showBattery) ) {
749 | v = 1
750 | state.ambientMap[state.weatherStationDataIndex].lastData["${k}"] = v
751 | }
752 | break
753 |
754 | default:
755 | break
756 | }
757 | // Post weather data as numeric values except for dates, etc
758 |
759 | okTOSendEvent = true
760 | switch (k) {
761 | case 'dateutc':
762 | okTOSendEvent = false
763 | break
764 | case 'date':
765 | v = state.ambientServerDate
766 | break
767 | case 'battin':
768 | k='battery'
769 | d.sendEvent(name: k, value: v.toInteger()*100, units:'%', displayed: false)
770 | break
771 | case 'battout':
772 | k='battery'
773 | v=v.toInteger()*100
774 | break
775 | case ~/^batt[0-9].*/:
776 | okTOSendEvent = false
777 | break
778 | case 'lastRain':
779 | v=convertToCurrentTimeZone(v)
780 | break
781 | case 'lightning_time':
782 | def lightning_datetime = new Date(v).toString()
783 | v=lightning_datetime
784 | break
785 | case 'tempf':
786 | k='temperature'
787 | break
788 | case ~/^feelsLike$|^feelsLikein$/:
789 | break
790 | case 'windspeedmph':
791 | // Send windSpeed as wind for Hubitat™
792 | d.sendEvent(name: "wind" , value: v , displayed: false)
793 | d.sendEvent(name: "windSpeed" , value: v , displayed: false)
794 | break
795 | case 'winddir':
796 | def winddirectionState = degToCompass(state.ambientMap[state.weatherStationDataIndex].lastData?.winddir, true)
797 | logDebug "Wind Direction -> ${winddirectionState}"
798 | d.sendEvent(name:'winddirection', value: winddirectionState, displayed: false)
799 | d.sendEvent(name:'wind_cardinal', value: degToCompass(state.ambientMap[state.weatherStationDataIndex].lastData?.winddir, false), displayed: false)
800 | d.sendEvent(name:'winddir2', value: winddirectionState + " (" + state.ambientMap[state.weatherStationDataIndex].lastData.winddir + "º)")
801 | // Send winddir as windVector for Hubitat™
802 | d.sendEvent(name:'windVector', value: state.ambientMap[state.weatherStationDataIndex].lastData?.winddir, displayed: false)
803 | d.sendEvent(name:'windDirection', value: state.ambientMap[state.weatherStationDataIndex].lastData?.winddir, displayed: false)
804 | break
805 | case 'uv':
806 | def UVInumRange
807 | switch (v) {
808 | case {it < 3}:
809 | UVInumRange="Low (${v})"
810 | break
811 | case {it < 6}:
812 | UVInumRange="Medium (${v})"
813 | break
814 | case {it < 8}:
815 | UVInumRange="High (${v})"
816 | break
817 | case {it < 11}:
818 | UVInumRange="Very High (${v})"
819 | break
820 | default:
821 | UVInumRange="Extreme (${v})"
822 | break
823 | }
824 | d.sendEvent(name: 'ultravioletIndexDisplay', value: UVInumRange )
825 | k='ultravioletIndex'
826 | break
827 | case 'yearlyrainin':
828 | k='totalrainin'
829 | break
830 | case 'solarradiation':
831 | logDebug "==> solarRadiation Raw = ${v}"
832 | logDebug "==> solarRadiation Decimal Format= ${solarRadiationDecimalFormat}"
833 | // Check to see if the user has set a decimal format for solar radiation
834 | if (solarRadiationDecimalFormat) {
835 | if (v.toInteger() > 0) {
836 | def formatSpecifier = "%." + solarRadiationDecimalFormat + "f"
837 | logDebug "==> formatSpecifier= ${formatSpecifier}"
838 | v = String.format(formatSpecifier, v)
839 | }
840 | }
841 | logDebug "==> solarRadiation decimal formatted = ${v}"
842 | switch(solarRadiationTileDisplayUnits) {
843 | case ('lux'):
844 | v = wm2lux(v)
845 | break
846 | case ('fc'):
847 | v = wm2fc(v)
848 | break
849 | default:
850 | break
851 | }
852 | d.sendEvent(name: 'solarradiation_display', value: sprintf("%s %s",v,solarRadiationTileDisplayUnits?:'W/m²'), units: solarRadiationTileDisplayUnits?:'W/m²')
853 | d.sendEvent(name: k, value: v, units: solarRadiationTileDisplayUnits?:'W/m²')
854 | k='illuminance'
855 | break
856 |
857 | // Weather Console Sensors
858 | case 'tempinf':
859 | remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}0")
860 | if (remoteSensorDNI) {
861 | logDebug "Posted temperature with value ${v} -> ${remoteSensorDNI}"
862 | remoteSensorDNI.sendEvent(name: "temperature", value: v, units: state.tempUnitsDisplay)
863 | remoteSensorDNI.sendEvent(name:"date", value: state.ambientServerDate, displayed: false)
864 | remoteSensorDNI.sendEvent(name:"lastSTupdate", value: tileLastUpdated(), displayed: false)
865 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey('battout') ) {
866 | remoteSensorDNI.sendEvent(name:"battery", value: state.ambientMap[state.weatherStationDataIndex].lastData.battout.toInteger()*100, displayed: false)
867 | }
868 | } else {
869 | logErr "Missing ${DTHDNIRemoteSensorName()}0"
870 | }
871 | break
872 | case 'humidityin':
873 | remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}0")
874 | if (remoteSensorDNI) {
875 | logDebug "Posted humidity with value ${v} -> ${remoteSensorDNI}"
876 | remoteSensorDNI.sendEvent(name: "humidity", value: v, units: "%", displayed: false)
877 | remoteSensorDNI.sendEvent(name: "humidity_display", value: "${v}%")
878 | } else {
879 | logErr "Missing ${DTHDNIRemoteSensorName()}0"
880 | }
881 | break
882 | // Post values for remote temperature & humidity sensors
883 | case ~/^temp[0-9][0-9]?f$|^soiltemp[0-9][0-9]?$/:
884 | def remoteIndexNumber = k.findAll( /\d+/ )[0]
885 | remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${remoteIndexNumber}")
886 | logDebug "${k} = ${remoteSensorDNI}"
887 | if (remoteSensorDNI) {
888 | logDebug "Posted temperature with value ${v} -> ${remoteSensorDNI}"
889 | remoteSensorDNI.sendEvent(name: "temperature", value: v, units: state.tempUnitsDisplay)
890 | remoteSensorDNI.sendEvent(name:"lastSTupdate", value: tileLastUpdated(), displayed: false)
891 | remoteSensorDNI.sendEvent(name:"date", value: state.ambientServerDate, displayed: false)
892 |
893 | String batteryFieldName = "batt" + remoteIndexNumber.toString()
894 | logDebug "batteryFieldName for '${k}' = ${batteryFieldName}"
895 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey(batteryFieldName)) {
896 | def battValue = state.ambientMap[state.weatherStationDataIndex].lastData."${batteryFieldName}".toInteger()*100
897 | logDebug "batteryFieldName = ${batteryFieldName} = ${battValue}%"
898 | remoteSensorDNI.sendEvent(name:"battery", value: battValue, displayed: false)
899 | }
900 |
901 | } else {
902 | logErr "Missing ST Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
903 | }
904 | okTOSendEvent = false
905 | break
906 | case ~/^dewPoint\d/:
907 | remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]}")
908 | logDebug "${k} = ${remoteSensorDNI}"
909 | if (remoteSensorDNI) {
910 | logDebug "Posted ${k.toUpperCase()}: ${v} -> ${remoteSensorDNI}"
911 | remoteSensorDNI.sendEvent(name: "dewpoint", value: v, units: state.tempUnitsDisplay)
912 | remoteSensorDNI.sendEvent(name: "dewPoint", value: v, units: state.tempUnitsDisplay)
913 | remoteSensorDNI.sendEvent(name: "dewPoint_display", value: "${v}${state.tempUnitsDisplay}", units: state.tempUnitsDisplay)
914 | } else {
915 | logErr "Missing HE Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
916 | }
917 | okTOSendEvent = false
918 | break
919 | case ~/^feelsLike\d/:
920 | remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]}")
921 | logDebug "Posted ${k.toUpperCase()} = ${remoteSensorDNI}"
922 | if (remoteSensorDNI) {
923 | logDebug "Posted ${k.toUpperCase()}: ${v} -> ${remoteSensorDNI}"
924 | remoteSensorDNI.sendEvent(name: "feelsLike", value: v, units: state.tempUnitsDisplay)
925 | remoteSensorDNI.sendEvent(name: "feelsLike_display", value: "${v}${state.tempUnitsDisplay}", units: state.tempUnitsDisplay)
926 | } else {
927 | logErr "Missing HE Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
928 | }
929 | okTOSendEvent = false
930 | break
931 | case ~/^feelsLikein\d/:
932 | remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]}")
933 | logDebug "Posted ${k.toUpperCase()} = ${remoteSensorDNI}"
934 | if (remoteSensorDNI) {
935 | logDebug "Posted ${k.toUpperCase()}: ${v} -> ${remoteSensorDNI}"
936 | remoteSensorDNI.sendEvent(name: "feelsLikein", value: v, units: state.tempUnitsDisplay)
937 | remoteSensorDNI.sendEvent(name: "feelsLikein_display", value: "${v}${state.tempUnitsDisplay}", units: state.tempUnitsDisplay)
938 | } else {
939 | logErr "Missing HE Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
940 | }
941 | okTOSendEvent = false
942 | break
943 | case ~/^humidity[0-9][0-9]?$|^soilhum[0-9][0-9]?$/:
944 | remoteSensorDNI = getChildDevice("${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]}")
945 | logDebug "Posted ${k.toUpperCase()} = ${remoteSensorDNI}"
946 | if (remoteSensorDNI) {
947 | logDebug "Posted humidity with value ${v} -> ${remoteSensorDNI}"
948 | remoteSensorDNI.sendEvent(name: "humidity", value: v, units: "%", displayed: false)
949 | remoteSensorDNI.sendEvent(name: "humidity_display", value: "${v}%")
950 | } else {
951 | logErr "Missing ST Device ${DTHDNIRemoteSensorName()}${k.findAll( /\d+/ )[0]} for ${k}"
952 | }
953 | okTOSendEvent = false
954 | break
955 | // Post values for Particle Monitor which report PM10/PM25/AQI/CO2
956 | case ~/(^pm.*)|(^co2.*)|(^aqi.*)/:
957 | remoteSensorDNI = getChildDevice("${DTHDNIPMName()}")
958 | logDebug "${k} = ${remoteSensorDNI}"
959 | if (remoteSensorDNI) {
960 | def sensorUnits = ''
961 | switch (k) {
962 | case ~/^aqi.*/:
963 | sensorUnits = ''
964 | break
965 | case ~/^co2.*/:
966 | sensorUnits = 'ppm'
967 | break
968 | case ~/^pm\d\d.*/:
969 | sensorUnits = 'µg/m3'
970 | break
971 | case ~/.*temp.*/:
972 | sensorUnits = '°F'
973 | break
974 | case ~/.*humidity.*/:
975 | sensorUnits = '%'
976 | break
977 | default:
978 | sensorUnits = ''
979 | break
980 | }
981 | logDebug "Posted ${k}: ${v} ${sensorUnits} -> ${remoteSensorDNI}"
982 | remoteSensorDNI.sendEvent(name: k, value: v, units: sensorUnits)
983 | remoteSensorDNI.sendEvent(name:"lastSTupdate", value: tileLastUpdated(), displayed: false)
984 | remoteSensorDNI.sendEvent(name:"date", value: state.ambientServerDate, displayed: false)
985 | if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey("batt_25")) {
986 | remoteSensorDNI.sendEvent(name:"battery", value: state.ambientMap[state.weatherStationDataIndex].lastData.batt_25.toInteger()*100, displayed: false)
987 | } else if (state.ambientMap[state.weatherStationDataIndex].lastData.containsKey("batt_co2")) {
988 | remoteSensorDNI.sendEvent(name:"battery", value: state.ambientMap[state.weatherStationDataIndex].lastData.batt_co2.toInteger()*100, displayed: false)
989 | }
990 | } else {
991 | logErr "Missing HE Device ${DTHDNIPMName()} for ${k}"
992 | }
993 | okTOSendEvent = false
994 | break
995 | default:
996 | break
997 | }
998 | if (okTOSendEvent){
999 | logDebug "okTOSendEvent: name: ${k} = value: ${v}"
1000 | switch (k) {
1001 | case ('battery'):
1002 | case ('date'):
1003 | d.sendEvent(name: k, value: v, displayed : false)
1004 | break
1005 | case ~/^temp.*/:
1006 | d.sendEvent(name: k, value: v, units: state.tempUnitsDisplay, displayed : false)
1007 | break
1008 | case ~/^feelsLike$|^feelsLikein$/:
1009 | d.sendEvent(name: k, value: v, units: state.tempUnitsDisplay)
1010 | break
1011 | case ('dewPointin'):
1012 | case ('dewPoint'):
1013 | d.sendEvent(name: k, value: v, units: state.tempUnitsDisplay, displayed : false)
1014 | d.sendEvent(name: k.toLowerCase(), value: v, units : state.tempUnitsDisplay, displayed: false )
1015 | break
1016 | case ('illuminance'):
1017 | d.sendEvent(name: k, value: v, units: solarRadiationTileDisplayUnits?:'W/m²', displayed : false)
1018 | break
1019 | case ~/^humidity.*/:
1020 | d.sendEvent(name: k, value: v, units: '%', displayed : false)
1021 | break
1022 | case ~/.*rain.*/:
1023 | d.sendEvent(name: k, value: v, units: state.measureUnitsDisplay, displayed : false)
1024 | break
1025 | case ('windir'):
1026 | d.sendEvent(name: k, value: v, units: 'º', displayed : false)
1027 | break
1028 | case ~/^wind.*/:
1029 | case ('maxdailygust'):
1030 | d.sendEvent(name: k, value: v, units: state.windUnitsDisplay, displayed : false)
1031 | break
1032 | case ~/^barom.*/:
1033 | d.sendEvent(name: k, value: v, units: state.baroUnitsDisplay, displayed : false)
1034 | break
1035 | default:
1036 | d.sendEvent(name: k, value: v)
1037 | break
1038 | }
1039 | }
1040 | }
1041 | } else {
1042 | logDebug "AmbientStationData did not return any weather data"
1043 | }
1044 | }
1045 |
1046 | def AmbientStationData(runID="????") {
1047 | def df = new java.text.SimpleDateFormat("hh:mm:ss a")
1048 | df.setTimeZone(location.timeZone)
1049 | def currentGETAmbientStationData = now()
1050 | state.lastGETAmbientStationData = state.lastGETAmbientStationData?:now()
1051 | logInfo "Start: AmbientStationData runID: ${runID} at ${df.format(new Date())}"
1052 | def timeSecsLastRun = (((currentGETAmbientStationData - state.lastGETAmbientStationData)/1000).toInteger())
1053 | logInfo "AmbientStationData Time difference is ${timeSecsLastRun} secs between last execution"
1054 | if (runID!=0 && timeSecsLastRun < 2) {
1055 | logWarn "Aborting AmbientStationData run ${runID}: Too Short for API Limits"
1056 | return
1057 | }
1058 | state.lastGETAmbientStationData = currentGETAmbientStationData
1059 | if(!state.apiKey){
1060 | logErr "Severe Error: The API key is UNDEFINED in ${app.name}'s IDE 'App Settings' field, fatal error now exiting"
1061 | return false
1062 | }
1063 | state.retry = state.retry?:0
1064 | scheduleCheckReset(true)
1065 | if (state.retry.toInteger()>0) {
1066 | logInfo "Executing Retry AmbientStationData re-attempt #${state.retry} for RunID: ${runID}"
1067 | }
1068 | def params = [
1069 | uri : "https://api.ambientweather.net",
1070 | path : "/v1/devices",
1071 | contentType : 'application/json',
1072 | query : [
1073 | "applicationKey" : APPKEY,
1074 | "apiKey" : state.apiKey
1075 | ]
1076 | ]
1077 | try {
1078 | httpGet(params) { resp ->
1079 | // get the data from the response body
1080 | state.respdata = resp.data
1081 | selectWeatherKeys(resp.data)
1082 | state.respStatus = resp.status
1083 | state.remove("httpError")
1084 | if (resp.status != 200) {
1085 | logErr "AmbientWeather.Net: response status code: ${resp.status}: response: ${resp.data}"
1086 | return false
1087 | }
1088 | if (state.weatherStationDataIndex) {
1089 | countRemoteTempHumiditySensors()
1090 | }
1091 | if (state.retry.toInteger()>0) {
1092 | logInfo "SUCCESS: Retry AmbientStationData re-attempt #${state.retry} for runID: ${runID}"
1093 | state.retry = 0
1094 | updateMyLabel('updated')
1095 | }
1096 | }
1097 | } catch (e) {
1098 | logDebug "Ambient Weather Station API Data runID ${runID}: ${e}"
1099 | resp?.headers.each {
1100 | logTrace "${it.name}: ${it.value}"
1101 | }
1102 | state.httpError = e.toString().toLowerCase()
1103 | if (e.toString().contains("unauthorized")) {
1104 | updateMyLabel('unauthorized')
1105 | return false
1106 | }
1107 | state.retry = state.retry.toInteger() + 1
1108 | if (state.retry.toInteger()<4) {
1109 | logInfo "Waiting 10 seconds to Try HttpGet Again runID ${runID}: Attempt #${state.retry}"
1110 | updateMyLabel('retry')
1111 | runIn(10, 'retryQuick', [overwrite: true, data: [runID: "${runID}"]])
1112 | }
1113 | return false
1114 | }
1115 | logInfo "SUCCESS: AmbientStationData successfully updated for runID: ${runID}"
1116 | updateMyLabel('updated')
1117 | return true
1118 | }
1119 |
1120 | def addAmbientChildDevice() {
1121 | // add Ambient Weather Reporter Station devices
1122 | // Derive a Short Name for the Weather Station and Remote Sensors
1123 | // Create/Validate Weather Console Device
1124 | def AWSName = "${state.weatherStationName}-Console"
1125 | def AWSLabel = "AWS-Console"
1126 | def AWSDNI = getChildDevice(state.deviceId)
1127 | if (!AWSDNI) {
1128 | logInfo "NEW: Adding Ambient Device: ${AWSName} with DNI: ${state.deviceId}"
1129 | try {
1130 | addChildDevice(DTHnamespace(), DTHName(), DTHDNI(), null, ["name": AWSName, "label": AWSLabel, completedSetup: true])
1131 | } catch(ex) {
1132 | logErr "The Ambient Weather Device Handler '${DTHName()}' was not found in your 'My Device Handlers', Error-> '${ex}'. Please run HPM Repair option for Ambient Weather Station"
1133 | return false
1134 | }
1135 | logInfo "Success: Added ${AWSName} with DNI: ${DTHDNI()}"
1136 | } else {
1137 | logInfo "Verified Weather Station '${getChildDevice(state.deviceId).label}' = DNI: '${DTHDNI()}'"
1138 | }
1139 |
1140 |
1141 | // add Ambient Weather Remote Sensor Device(s)
1142 | def remoteSensorNamePref
1143 | def remoteSensorLabelPref
1144 | def remoteSensorNameDNI
1145 | def remoteSensorNumber
1146 | settings.each { key, value ->
1147 | if ( key.startsWith(DTHDNIRemoteSensorName()) ) {
1148 | remoteSensorNamePref = "${state.weatherStationName}-${value}"
1149 | remoteSensorLabelPref = "AWS-${value}"
1150 | remoteSensorNameDNI = getChildDevice(key)
1151 | remoteSensorNumber = key.reverse()[0..0]
1152 | if (remoteSensorNumber.toInteger() <= MaxNumRemoteSensors()) {
1153 | if (!remoteSensorNameDNI) {
1154 | logInfo "NEW: Adding Remote Sensor #${remoteSensorNumber}: ${remoteSensorLabelPref}"
1155 | try {
1156 | addChildDevice(DTHnamespace(), DTHRemoteSensorName(), "${key}", null, ["name": remoteSensorNamePref, "label": remoteSensorLabelPref, completedSetup: true])
1157 | } catch(ex) {
1158 | logErr "The Ambient Weather Device Handler '${DTHRemoteSensorName()}' was not found in your 'My Device Handlers', Error-> '${ex}'. Please run HPM Repair option for Ambient Weather Station."
1159 | return false
1160 | }
1161 | logInfo "Success Added Ambient Remote Sensor: ${remoteSensorLabelPref} with DNI: ${key}"
1162 | } else {
1163 | logInfo "Verified Remote Sensor #${remoteSensorNumber} of ${state.countRemoteTempHumiditySensors} Exists: ${getChildDevice(key).label} = DNI: ${key}"
1164 | // Update Device Label Values
1165 | if (remoteSensorNameDNI.label != value) {
1166 | logInfo "Renaming Device ${remoteSensorNameDNI.deviceNetworkId}: Old Device Label: ${remoteSensorNameDNI.label} → New Device Label: ${value}"
1167 | remoteSensorNameDNI.label = value
1168 | }
1169 | }
1170 | } else {
1171 | logWarn "Device ${remoteSensorNumber} DNI: ${key} '${remoteSensorNameDNI.name}' exceeds # of remote sensors (${state.countRemoteTempHumiditySensors}) reporting from Ambient -> ACTION REQUIRED"
1172 | logWarn "Please verify that all Ambient Remote Sensors are online and reporting to Ambient Network. If so, please manually delete the device in the 'Devices' view"
1173 | }
1174 | }
1175 | }
1176 |
1177 | // add Ambient Weather Particulate Monitor Device(s)
1178 | def PMkey = "${DTHDNIPMName()}"
1179 | def PMvalue = settings.find{ it.key == "${DTHDNIPMName()}" }?.value
1180 |
1181 | if(PMvalue) {
1182 | remoteSensorNamePref = "${state.weatherStationName}${PMvalue?'-'+PMvalue:''}"
1183 | remoteSensorLabelPref = "AWS-${PMvalue}"
1184 | remoteSensorNameDNI = getChildDevice(PMkey)
1185 | if (!remoteSensorNameDNI) {
1186 | logInfo "NEW: Adding Particulate Monitor device: ${remoteSensorLabelPref}"
1187 | try {
1188 | addChildDevice(DTHnamespace(), DTHPMSensorName(), "${PMkey}", null, ["name": remoteSensorNamePref, "label": remoteSensorLabelPref, completedSetup: true])
1189 | } catch(ex) {
1190 | logErr "The Ambient Weather Device Handler '${DTHPMSensorName()}' was not found in your 'My Device Handlers', Error-> '${ex}'. Please install this in the IDE's 'My Device Handlers'"
1191 | return false
1192 | }
1193 | logInfo "Success Added Ambient Particulate Monitor: ${remoteSensorLabelPref} with DNI: ${PMkey}"
1194 | } else {
1195 | if(infoVerbose){logInfo "Verified Particulate Monitor ${state.countParticulateMonitors} Exists: ${remoteSensorLabelPref} = DNI: ${PMkey}"}
1196 | // Update Device Label Values
1197 | if (remoteSensorNameDNI.label != PMvalue) {
1198 | logInfo "Renaming Device ${remoteSensorNameDNI.deviceNetworkId}: Old Device Label: ${remoteSensorNameDNI.label} → New Device Label: ${PMvalue}"
1199 | remoteSensorNameDNI.label = PMvalue
1200 | }
1201 | }
1202 | }
1203 | }
1204 |
1205 | def degToCompass(num,longTitles=true) {
1206 | if (num) {
1207 | def val = Math.floor((num.toFloat() / 22.5) + 0.5).toInteger()
1208 | def arr = []
1209 | if (longTitles) {
1210 | arr = ["N", "North NE", "NE", "East NE", "E", "East SE", "SE", "South SE", "S", "South SW", "SW", "West SW", "W", "West NW", "NW", "North NW"]
1211 | } else {
1212 | arr = ["N", "N NE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
1213 | }
1214 | return arr[(val % 16)]
1215 | }
1216 | return "N/A"
1217 | }
1218 |
1219 |
1220 | def setScheduler(schedulerFreq) {
1221 | def scheduleHandler = 'autoScheduleHandler'
1222 | unschedule(scheduleHandler)
1223 | def randonInt = Math.abs(new Random().nextInt() % 59) + 1
1224 | if(infoVerbose){logInfo "Auto Schedule Refresh Rate is now -> ${schedulerFreq} mins"}
1225 | switch(schedulerFreq) {
1226 | case '0':
1227 | logInfo "Auto Schedule Refresh Rate is now: OFF"
1228 | break
1229 | case '1':
1230 | runEvery1Minute(scheduleHandler)
1231 | break
1232 | case '2':
1233 | schedule("0 ${randonInt}/2 * * * ?",scheduleHandler)
1234 | break
1235 | case '3':
1236 | schedule("0 ${randonInt}/3 * * * ?",scheduleHandler)
1237 | break
1238 | case '4':
1239 | schedule("0 ${randonInt}/4 * * * ?",scheduleHandler)
1240 | break
1241 | case '5':
1242 | runEvery5Minutes(scheduleHandler)
1243 | break
1244 | case '10':
1245 | runEvery10Minutes(scheduleHandler)
1246 | break
1247 | case '15':
1248 | runEvery15Minutes(scheduleHandler)
1249 | break
1250 | case '30':
1251 | runEvery30Minutes(scheduleHandler)
1252 | break
1253 | case '60':
1254 | runEvery1Hour(scheduleHandler)
1255 | break
1256 | case '120':
1257 | schedule("0 ${randonInt} 0/2 * * ?",scheduleHandler)
1258 | break
1259 | case '180':
1260 | runEvery3Hours(scheduleHandler)
1261 | break
1262 | default :
1263 | unschedule()
1264 | break
1265 | }
1266 | }
1267 |
1268 | def tileLastUpdated() {
1269 | def now = new Date().format('EEE MMM dd, hh:mm:ss a',location.timeZone)
1270 | return now
1271 | }
1272 | def informationList(variable) {
1273 | switch(variable) {
1274 | case ("apiHelp") :
1275 | // Help Text for API Key
1276 | variable = [
1277 | "You MUST enter your Ambient Weather API key in the ${appNameVersion()}.",
1278 | "Visit your Ambient Weather Dashboards's Account page.",
1279 | "Create/Copy your API key from the bottom of the page",
1280 | "Return to your Hubitat App.",
1281 | "Exit the SmartApp and Start ${appNameVersion()} Setup again."
1282 | ]
1283 | break
1284 | default:
1285 | break
1286 | }
1287 | if (variable instanceof List) {
1288 | def numberedText = ""
1289 | variable.eachWithIndex { item, index ->
1290 | numberedText += "${index+1}. ${item}"
1291 | numberedText += (index
1318 | if (k.value) {
1319 | date = new Date(k.value).format("MMM-DD-YYYY h:mm a", location.timeZone)
1320 | dateToday = new Date(k.value).format("MMM-DD-YYYY", location.timeZone)
1321 | if (today==dateToday) {
1322 | date = "Today @ " + new Date(k.value).format("h:mm a", location.timeZone)
1323 | }
1324 | msg += "${index+1}) ${k.key} : ${date}\n"
1325 | } else {
1326 | msg += "${index+1}) ${k.key} : --\n"
1327 | }
1328 | }
1329 | return msg
1330 | }
1331 |
1332 | def notifyEvents() {
1333 | if (checkRequired([pushoverEnabled,sendSMSEnabled,sendPushEnabled])){
1334 | def now = now()
1335 | def msg
1336 | def tempCheck = state.ambientMap[state.weatherStationDataIndex].lastData.tempf?:state.ambientMap[state.weatherStationDataIndex].lastData.temp1f
1337 | def ambientWeatherStationName = "${DTHName()} - '${state.weatherStationName}'"
1338 | if ( (notifyAlertLowTemp) && (tempCheck) && (tempCheck.toInteger()<=notifyAlertLowTemp) ) {
1339 | msg = "${ambientWeatherStationName}: LOW TEMP ALERT: Current temperature of ${tempCheck}${state.tempUnitsDisplay} <= ${notifyAlertLowTemp}${state.tempUnitsDisplay}"
1340 | if (lastNotifyDT(state.notifyAlertLowTempDT, "Low Temp")) {
1341 | send_message(msg)
1342 | state.notifyAlertLowTempDT = now
1343 | }
1344 | }
1345 | if ( (notifyAlertHighTemp) && (tempCheck) && (tempCheck.toInteger()>=notifyAlertHighTemp) ) {
1346 | msg = "${ambientWeatherStationName}: HIGH TEMP ALERT: Current temperature of ${tempCheck}${state.tempUnitsDisplay} >= ${notifyAlertHighTemp}${state.tempUnitsDisplay}"
1347 | if (lastNotifyDT(state.notifyAlertHighTempDT, "High Temp")) {
1348 | state.notifyAlertHighTempDT = now
1349 | send_message(msg)
1350 | }
1351 | }
1352 | if ( (notifyRain) && (state.ambientMap[state.weatherStationDataIndex].lastData.hourlyrainin) && (state.ambientMap[state.weatherStationDataIndex].lastData.hourlyrainin.toFloat()>0) ){
1353 | msg = "${ambientWeatherStationName}: RAIN DETECTED ALERT: Current hourly rain sensor reading of ${state.ambientMap[state.weatherStationDataIndex].lastData.hourlyrainin} ${state.measureUnitsDisplay}/hr"
1354 | if (lastNotifyDT(state.notifyRainDT, "Rain")) {
1355 | state.notifyRainDT = now
1356 | send_message(msg)
1357 | }
1358 | }
1359 | }
1360 | }
1361 |
1362 | def lastNotifyDT(lastDT, eventName) {
1363 | if (!lastDT) { return true }
1364 | def now = now()/1000
1365 | def date = new Date(lastDT).format("MMM-DD-YYYY h:mm:ss a", location.timeZone)
1366 | def hours = ((now-(lastDT/1000))/3600).toFloat().round(1)
1367 | def days = (hours/24).toFloat().round(1)
1368 | def rc = hours>=state.notifyAlertFreq.toInteger()
1369 | logInfo "This '${eventName}' event was last sent on ${date}: ${days} days, ${hours} hours ago"
1370 | logInfo "${eventName} Alert Every ${notifyAlertFreq} hours: ${rc?'OK to SMS':'TOO EARLY TO SEND'}"
1371 | return rc
1372 | }
1373 |
1374 | def convertStateWeatherStationData() {
1375 | // Check to see if Units of Measure have been defined in the preferences section, otherwise default to Hub's location for imperial or metric
1376 | if (tempUnits == null) {
1377 | def tempUnitsSmartThingsScale = getTemperatureScale()
1378 | logWarn "Missing 'Units of Measure' App Preference Setting Values: ALL Default Units of Measure will be based on your hub's location temperature preference of '${tempUnitsSmartThingsScale}'"
1379 | logWarn "Please run '${state.weatherStationName}' SmartAPP install to select your default Units of Measure for display"
1380 | state.tempUnitsDisplay = "°${tempUnitsSmartThingsScale}"
1381 | state.windUnitsDisplay = (tempUnitsSmartThingsScale == "F") ? "mph" : "kph"
1382 | state.measureUnitsDisplay = (tempUnitsSmartThingsScale == "F") ? "in" : "cm"
1383 | state.baroUnitsDisplay = (tempUnitsSmartThingsScale == "F") ? "inHg" : "mmHg"
1384 | }
1385 | logDebug "tempUnitsDisplay = ${state.tempUnitsDisplay}, windUnitsDisplay = ${state.windUnitsDisplay}, measureUnitsDisplay = ${state.measureUnitsDisplay}, baroUnitsDisplay = ${state.baroUnitsDisplay}"
1386 | def tempVar = null
1387 | def newAmbientMap = [:]
1388 | newAmbientMap = state.ambientMap
1389 | newAmbientMap[state.weatherStationDataIndex].lastData.each{ k, v ->
1390 | tempVar = null
1391 | switch (k) {
1392 | case ~/^temp.*/:
1393 | case ~/^feelsLike.*/:
1394 | case 'dewPoint':
1395 | if (state.tempUnitsDisplay == '°C') {
1396 | tempVar = String.format("%.01f",(v-32)*5/9)
1397 | }
1398 | break
1399 | case ~/.*rain.*/:
1400 | try {
1401 | if (state.measureUnitsDisplay == 'cm') {
1402 | tempVar = String.format("%.02f",v*2.54)
1403 | } else if (state.measureUnitsDisplay == 'mm') {
1404 | tempVar = String.format("%.02f",v*25.4)
1405 | }
1406 | } catch(Exception ex) {
1407 | tempVar = 0
1408 | }
1409 | break
1410 | case ~/^winddir.*/:
1411 | break
1412 | case ~/^wind.*/:
1413 | case ('maxdailygust'):
1414 | if (state.windUnitsDisplay == 'kph') {
1415 | tempVar = String.format("%.02f",v*1.609344)
1416 | } else if (state.windUnitsDisplay == 'fps') {
1417 | tempVar = String.format("%.02f",v*2/3)
1418 | } else if (state.windUnitsDisplay == 'mps') {
1419 | tempVar = String.format("%.02f",v*0.44704)
1420 | } else if (state.windUnitsDisplay == 'knotts') {
1421 | tempVar = String.format("%.02f",v*0.86898)
1422 | }
1423 | break
1424 | case ~/^barom.*/:
1425 | if (state.baroUnitsDisplay == 'mmHg') {
1426 | tempVar = String.format("%.02f",v*25.4)
1427 | } else if (state.baroUnitsDisplay == 'hPa') {
1428 | tempVar = String.format("%.02f",v*33.86389)
1429 | }
1430 | break
1431 | default:
1432 | break
1433 | }
1434 | if(tempVar != null) {
1435 | logDebug "tempVar k=${k}, v=${v} tempVar=${tempVar}"
1436 | newAmbientMap[state.weatherStationDataIndex].lastData."${k}" = tempVar.toFloat()
1437 | }
1438 | }
1439 | state.ambientMap = [:]
1440 | state.ambientMap = newAmbientMap
1441 | }
1442 |
1443 | def setStateWeatherStationData() {
1444 | if (weatherStationMac) {
1445 | state.weatherStationDataIndex = state.ambientMap.findIndexOf {
1446 | it.macAddress in [weatherStationMac]
1447 | }
1448 | }
1449 | state.weatherStationDataIndex = state.weatherStationDataIndex?:0
1450 | state.weatherStationMac = state.weatherStationMac?:state.ambientMap[state.weatherStationDataIndex].macAddress
1451 | state.weatherStationName = state.ambientMap[state.weatherStationDataIndex].info.name
1452 | state.weatherStationLocation = state.ambientMap[state.weatherStationDataIndex].info.location?:state.ambientMap[state.weatherStationDataIndex].info.containsKey("coords")?state.ambientMap[state.weatherStationDataIndex].info.coords.location:''
1453 | countRemoteTempHumiditySensors()
1454 | countParticulateMonitors()
1455 | }
1456 |
1457 | def countRemoteTempHumiditySensors() {
1458 | state.countRemoteTempHumiditySensors = state.ambientMap[state.weatherStationDataIndex].lastData.keySet().count { it.matches('^temp[0-9][0-9]?f|^soiltemp[0-9]?[0-9]?|^soilhum[0-9]?[0-9]?') }
1459 | return state.countRemoteTempHumiditySensors
1460 | }
1461 |
1462 | def countParticulateMonitors() {
1463 | def pmDevice = state.ambientMap[state.weatherStationDataIndex].lastData?.keySet().count { it.matches('^pm.*') }
1464 | if (pmDevice == 0) state.countParticulateMonitors = 0
1465 | else state.countParticulateMonitors = 1
1466 | return state.countParticulateMonitors
1467 | }
1468 |
1469 | def unitsSet() {
1470 | if ([tempUnits, windUnits, measureUnits, baroUnits, solarRadiationTileDisplayUnits].findAll({it != null}).join()=='') return "Tap to Select"
1471 | return sprintf("%s, %s, %s, %s, %s", tempUnits, windUnits, measureUnits, baroUnits, solarRadiationTileDisplayUnits)
1472 | }
1473 |
1474 | def alertFilterList() {
1475 | def x = [
1476 | "ABV":"ABV - Rawinsonde Data Above 100 Millibars",
1477 | "ADA":"ADA - Alarm/Alert Administrative Msg",
1478 | "ADM":"ADM - Alert Administrative Message",
1479 | "ADR":"ADR - NWS Administrative Message",
1480 | "ADV":"ADV - Generic Space Environment Advisory",
1481 | "AFD":"AFD - Area Forecast Discussion",
1482 | "AFM":"AFM - Area Forecast Matrices",
1483 | "AFP":"AFP - Area Forecast Product",
1484 | "AFW":"AFW - Fire Weather Matrix",
1485 | "AGF":"AGF - Agricultural Forecast",
1486 | "AGO":"AGO - Agricultural Observations",
1487 | "ALT":"ALT - Space Environment Alert",
1488 | "AQA":"AQA - Air Quality Alert",
1489 | "AQI":"AQI - Air Quality Index Statement",
1490 | "ASA":"ASA - Air Stagnation Advisory",
1491 | "AVA":"AVA - Avalanche Watch",
1492 | "AVW":"AVW - Avalanche Warning",
1493 | "AWO":"AWO - Area Weather Outlook",
1494 | "AWS":"AWS - Area Weather Summary",
1495 | "AWU":"AWU - Area Weather Update",
1496 | "AWW":"AWW - Airport Weather Warning",
1497 | "BOY":"BOY - Buoy Report",
1498 | "BRG":"BRG - Coast Guard Observations",
1499 | "BRT":"BRT - Hourly Roundup for Weather Radio",
1500 | "CAE":"CAE - Child Abduction Emergency",
1501 | "CCF":"CCF - Coded City Forecast",
1502 | "CDW":"CDW - Civil Danger Warning",
1503 | "CEM":"CEM - Civil Emergency Message",
1504 | "CF6":"CF6 - WFO Monthly/Daily Climate Data",
1505 | "CFP":"CFP - Convective Forecast Product",
1506 | "CFW":"CFW - Coastal Flood Warnings/Watches/Statements",
1507 | "CGR":"CGR - Coast Guard Surface Report",
1508 | "CHG":"CHG - Computer Hurricane Guidance",
1509 | "CLA":"CLA - Climatological Report (Annual)",
1510 | "CLI":"CLI - Climatological Report (Daily)",
1511 | "CLM":"CLM - Climatological Report (Monthly)",
1512 | "CLQ":"CLQ - Climatological Report (Quarterly)",
1513 | "CLS":"CLS - Climatological Report (Seasonal)",
1514 | "CLT":"CLT - Climate Report",
1515 | "CMM":"CMM - Coded Climatological Monthly Means",
1516 | "COD":"COD - Coded Analysis and Forecasts",
1517 | "CPF":"CPF - Great Lakes Port Forecast",
1518 | "CUR":"CUR - Routine Space Environment Products",
1519 | "CWA":"CWA - Center (CWSU) Weather Advisory",
1520 | "CWF":"CWF - Coastal Waters Forecast",
1521 | "CWS":"CWS - Center (CWSU) Weather Statement",
1522 | "DAY":"DAY - Routine Space Environment Product (Daily)",
1523 | "DDO":"DDO - Daily Dispersion Outlook",
1524 | "DGT":"DGT - Drought Information Statement",
1525 | "DSA":"DSA - Unnumbered Depression / Suspicious Area Advisory",
1526 | "DSM":"DSM - ASOS Daily Summary",
1527 | "DSW":"DSW - Dust Storm Warning and Dust Advisory",
1528 | "EFP":"EFP - 3 To 5 Day Extended Forecast",
1529 | "EOL":"EOL - Average 6 To 10 Day Weather Outlook (Local)",
1530 | "EQI":"EQI - Tsunami Bulletin",
1531 | "EQR":"EQR - Earthquake Report",
1532 | "EQW":"EQW - Earthquake Warning",
1533 | "ESF":"ESF - Flood Potential Outlook",
1534 | "ESG":"ESG - Extended Streamflow Guidance",
1535 | "ESP":"ESP - Extended Streamflow Prediction",
1536 | "ESS":"ESS - Water Supply Outlook",
1537 | "EVI":"EVI - Evacuation Immediate",
1538 | "EWW":"EWW - Extreme Wind Warning",
1539 | "FA0":"FA0 - Aviation Area Forecasts (Pacific)",
1540 | "FA1":"FA1 - Aviation Area Forecasts (Northeast)",
1541 | "FA2":"FA2 - Aviation Area Forecasts (Southeast)",
1542 | "FA3":"FA3 - Aviation Area Forecasts (North Central)",
1543 | "FA4":"FA4 - Aviation Area Forecasts (South Central)",
1544 | "FA5":"FA5 - Aviation Area Forecasts (Rocky Mountains)",
1545 | "FA6":"FA6 - Aviation Area Forecasts (West Coast)",
1546 | "FA7":"FA7 - Aviation Area Forecasts (Juneau, AK)",
1547 | "FA8":"FA8 - Aviation Area Forecasts (Anchorage, AK)",
1548 | "FA9":"FA9 - Aviation Area Forecasts (Fairbanks, AK)",
1549 | "FD0":"FD0 - 24 Hr Fd Winds Aloft Fcst (45,000 and 53,000 Ft)",
1550 | "FD1":"FD1 - 6 Hour Winds Aloft Forecast",
1551 | "FD2":"FD2 - 12 Hour Winds Aloft Forecast",
1552 | "FD3":"FD3 - 24 Hour Winds Aloft Forecast",
1553 | "FD4":"FD4 - Winds Aloft Forecast",
1554 | "FD5":"FD5 - Winds Aloft Forecast",
1555 | "FD6":"FD6 - Winds Aloft Forecast",
1556 | "FD7":"FD7 - Winds Aloft Forecast",
1557 | "FD8":"FD8 - 6 Hour Fd Winds Aloft Fcst (45,000 and 53,000 Ft)",
1558 | "FD9":"FD9 - 12 Hr Fd Winds Aloft Fcst (45,000 and 53,000 Ft)",
1559 | "FDI":"FDI - Fire Danger Indices",
1560 | "FFA":"FFA - Flash Flood Watch",
1561 | "FFG":"FFG - Flash Flood Guidance",
1562 | "FFH":"FFH - Headwater Guidance",
1563 | "FFS":"FFS - Flash Flood Statement",
1564 | "FFW":"FFW - Flash Flood Warning",
1565 | "FLN":"FLN - National Flood Summary",
1566 | "FLS":"FLS - Flood Statement",
1567 | "FLW":"FLW - Flood Warning",
1568 | "FOF":"FOF - Upper Wind Fallout Forecast",
1569 | "FRW":"FRW - Fire Warning",
1570 | "FSH":"FSH - Natl Marine Fisheries Administrative Service Message",
1571 | "FTM":"FTM - WSR-88D Radar Outage Notification / Free Text Message",
1572 | "FTP":"FTP - FOUS Prog Max/Min Temp/Pop Guidance",
1573 | "FWA":"FWA - Fire Weather Administrative Message",
1574 | "FWD":"FWD - Fire Weather Outlook Discussion",
1575 | "FWF":"FWF - Routine Fire Wx Fcst (With/Without 6-10 Day Outlook)",
1576 | "FWL":"FWL - Land Management Forecasts",
1577 | "FWM":"FWM - Miscellaneous Fire Weather Product",
1578 | "FWN":"FWN - Fire Weather Notification",
1579 | "FWO":"FWO - Fire Weather Observation",
1580 | "FWS":"FWS - Suppression Forecast",
1581 | "FZL":"FZL - Freezing Level Data (RADAT)",
1582 | "GLF":"GLF - Great Lakes Forecast",
1583 | "GLS":"GLS - Great Lakes Storm Summary",
1584 | "GRE":"GRE - GREEN",
1585 | "HD1":"HD1 - RFC Derived QPF Data Product",
1586 | "HD2":"HD2 - RFC Derived QPF Data Product",
1587 | "HD3":"HD3 - RFC Derived QPF Data Product",
1588 | "HD4":"HD4 - RFC Derived QPF Data Product",
1589 | "HD7":"HD7 - RFC Derived QPF Data Product",
1590 | "HD8":"HD8 - RFC Derived QPF Data Product",
1591 | "HD9":"HD9 - RFC Derived QPF Data Product",
1592 | "HLS":"HLS - Hurricane Local Statement",
1593 | "HMD":"HMD - Hydrometeorological Discussion",
1594 | "HML":"HML - AHPS XML",
1595 | "HMW":"HMW - Hazardous Materials Warning",
1596 | "HP1":"HP1 - RFC QPF Verification Product",
1597 | "HP2":"HP2 - RFC QPF Verification Product",
1598 | "HP3":"HP3 - RFC QPF Verification Product",
1599 | "HP4":"HP4 - RFC QPF Verification Product",
1600 | "HP5":"HP5 - RFC QPF Verification Product",
1601 | "HP6":"HP6 - RFC QPF Verification Product",
1602 | "HP7":"HP7 - RFC QPF Verification Product",
1603 | "HP8":"HP8 - RFC QPF Verification Product",
1604 | "HRR":"HRR - Weather Roundup",
1605 | "HSF":"HSF - High Seas Forecast",
1606 | "HWO":"HWO - Hazardous Weather Outlook",
1607 | "HWR":"HWR - Hourly Weather Roundup",
1608 | "HYD":"HYD - Daily Hydrometeorological Products",
1609 | "HYM":"HYM - Monthly Hydrometeorological Plain Language Product",
1610 | "ICE":"ICE - Ice Forecast",
1611 | "IDM":"IDM - Ice Drift Vectors",
1612 | "INI":"INI - ADMINISTR [NOUS51 KWBC]",
1613 | "IOB":"IOB - Ice Observation",
1614 | "KPA":"KPA - Keep Alive Message",
1615 | "LAE":"LAE - Local Area Emergency",
1616 | "LCD":"LCD - Preliminary Local Climatological Data",
1617 | "LCO":"LCO - Local Cooperative Observation",
1618 | "LEW":"LEW - Law Enforcement Warning",
1619 | "LFP":"LFP - Local Forecast",
1620 | "LKE":"LKE - Lake Stages",
1621 | "LLS":"LLS - Low-Level Sounding",
1622 | "LOW":"LOW - Low Temperatures",
1623 | "LSR":"LSR - Local Storm Report",
1624 | "LTG":"LTG - Lightning Data",
1625 | "MAN":"MAN - Rawinsonde Observation Mandatory Levels",
1626 | "MAP":"MAP - Mean Areal Precipitation",
1627 | "MAW":"MAW - Amended Marine Forecast",
1628 | "MFM":"MFM - Marine Forecast Matrix",
1629 | "MIM":"MIM - Marine Interpretation Message",
1630 | "MIS":"MIS - Miscellaneous Local Product",
1631 | "MOB":"MOB - MOB Observations",
1632 | "MON":"MON - Routine Space Environment Product Issued Monthly",
1633 | "MRP":"MRP - Techniques Development Laboratory Marine Product",
1634 | "MSM":"MSM - ASOS Monthly Summary Message",
1635 | "MTR":"MTR - METAR Formatted Surface Weather Observation",
1636 | "MTT":"MTT - METAR Test Message",
1637 | "MVF":"MVF - Marine Verification Coded Message",
1638 | "MWS":"MWS - Marine Weather Statement",
1639 | "MWW":"MWW - Marine Weather Message",
1640 | "NOU":"NOU - Weather Reconnaisance Flights",
1641 | "NOW":"NOW - Short Term Forecast",
1642 | "NOX":"NOX - Data Mgt Message",
1643 | "NPW":"NPW - Non-Precipitation Warnings / Watches / Advisories",
1644 | "NSH":"NSH - Nearshore Marine Forecast",
1645 | "NUW":"NUW - Nuclear Power Plant Warning",
1646 | "NWR":"NWR - NOAA Weather Radio Forecast",
1647 | "OAV":"OAV - Other Aviation Products",
1648 | "OBS":"OBS - Observations",
1649 | "OFA":"OFA - Offshore Aviation Area Forecast",
1650 | "OFF":"OFF - Offshore Forecast",
1651 | "OMR":"OMR - Other Marine Products",
1652 | "OPU":"OPU - Other Public Products",
1653 | "OSO":"OSO - Other Surface Observations",
1654 | "OSW":"OSW - Ocean Surface Winds",
1655 | "OUA":"OUA - Other Upper Air Data",
1656 | "OZF":"OZF - Zone Forecast",
1657 | "PFM":"PFM - Point Forecast Matrices",
1658 | "PFW":"PFW - Fire Weather Point Forecast Matrices",
1659 | "PLS":"PLS - Plain Language Ship Report",
1660 | "PMD":"PMD - Prognostic Meteorological Discussion",
1661 | "PNS":"PNS - Public Information Statement",
1662 | "POE":"POE - Probability of Exceed",
1663 | "PRB":"PRB - Heat Index Forecast Tables",
1664 | "PRC":"PRC - State Pilot Report Collective",
1665 | "PRE":"PRE - Preliminary Forecasts",
1666 | "PSH":"PSH - Post Storm Hurricane Report",
1667 | "PTS":"PTS - Probabilistic Outlook Points",
1668 | "PWO":"PWO - Public Severe Weather Outlook",
1669 | "PWS":"PWS - Tropical Cyclone Probabilities",
1670 | "QPF":"QPF - Quantitative Precipitation Forecast",
1671 | "QPS":"QPS - Quantitative Precipitation Statement",
1672 | "RDF":"RDF - Revised Digital Forecast",
1673 | "REC":"REC - Recreational Report",
1674 | "RER":"RER - Record Report",
1675 | "RET":"RET - EAS Activation Request",
1676 | "RFD":"RFD - Rangeland Fire Danger Forecast",
1677 | "RFI":"RFI - RFI Observation",
1678 | "RFR":"RFR - Route Forecast",
1679 | "RFW":"RFW - Red Flag Warning",
1680 | "RHW":"RHW - Radiological Hazard Warning",
1681 | "RNS":"RNS - Rain Information Statement",
1682 | "RR1":"RR1 - Hydro-Met Data Report Part 1",
1683 | "RR2":"RR2 - Hydro-Met Data Report Part 2",
1684 | "RR3":"RR3 - Hydro-Met Data Report Part 3",
1685 | "RR4":"RR4 - Hydro-Met Data Report Part 4",
1686 | "RR5":"RR5 - Hydro-Met Data Report Part 5",
1687 | "RR6":"RR6 - Hydro-Met Data Report Part 6",
1688 | "RR7":"RR7 - Hydro-Met Data Report Part 7",
1689 | "RR8":"RR8 - Hydro-Met Data Report Part 8",
1690 | "RR9":"RR9 - Hydro-Met Data Report Part 9",
1691 | "RRA":"RRA - Automated Hydrologic Observation Sta Report (AHOS)",
1692 | "RRM":"RRM - Miscellaneous Hydrologic Data",
1693 | "RRS":"RRS - HADS Data",
1694 | "RRY":"RRY - ASOS SHEF Hourly Routine Test Message",
1695 | "RSD":"RSD - Daily Snotel Data",
1696 | "RSM":"RSM - Monthly Snotel Data",
1697 | "RTP":"RTP - Regional Max/Min Temp and Precipitation Table",
1698 | "RVA":"RVA - River Summary",
1699 | "RVD":"RVD - Daily River Forecasts",
1700 | "RVF":"RVF - River Forecast",
1701 | "RVI":"RVI - River Ice Statement",
1702 | "RVM":"RVM - Miscellaneous River Product",
1703 | "RVR":"RVR - River Recreation Statement",
1704 | "RVS":"RVS - River Statement",
1705 | "RWR":"RWR - Regional Weather Roundup",
1706 | "RWS":"RWS - Regional Weather Summary",
1707 | "SAB":"SAB - Special Avalanche Bulletin",
1708 | "SAF":"SAF - Speci Agri Wx Fcst / Advisory / Flying Farmer Fcst Outlook",
1709 | "SAG":"SAG - Snow Avalanche Guidance",
1710 | "SAT":"SAT - APT Prediction",
1711 | "SAW":"SAW - Prelim Notice of Watch & Cancellation Msg (Aviation)",
1712 | "SCC":"SCC - Storm Summary",
1713 | "SCD":"SCD - Supplementary Climatological Data (ASOS)",
1714 | "SCN":"SCN - Soil Climate Analysis Network Data",
1715 | "SCP":"SCP - Satellite Cloud Product",
1716 | "SCS":"SCS - Selected Cities Summary",
1717 | "SDO":"SDO - Supplementary Data Observation (ASOS)",
1718 | "SDS":"SDS - Special Dispersion Statement",
1719 | "SEL":"SEL - Severe Local Storm Watch and Watch Cancellation Msg",
1720 | "SEV":"SEV - SPC Watch Point Information Message",
1721 | "SFP":"SFP - State Forecast",
1722 | "SFT":"SFT - Tabular State Forecast",
1723 | "SGL":"SGL - Rawinsonde Observation Significant Levels",
1724 | "SHP":"SHP - Surface Ship Report at Synoptic Time",
1725 | "SIG":"SIG - International Sigmet / Convective Sigmet",
1726 | "SIM":"SIM - Satellite Interpretation Message",
1727 | "SLS":"SLS - Severe Local Storm Watch and Areal Outline",
1728 | "SMF":"SMF - Smoke Management Weather Forecast",
1729 | "SMW":"SMW - Special Marine Warning",
1730 | "SOO":"SOO - SOO Product",
1731 | "SPE":"SPE - Satellite Precipitation Estimates (TXUS20 KWBC)",
1732 | "SPF":"SPF - Storm Strike Probability Bulletin (TPC)",
1733 | "SPS":"SPS - Special Weather Statement",
1734 | "SPW":"SPW - Shelter in Place Warning",
1735 | "SQW":"SQW - Snow Squall Warning",
1736 | "SRD":"SRD - Surf Discussion",
1737 | "SRF":"SRF - Surf Forecast",
1738 | "SRG":"SRG - Soaring Guidance",
1739 | "SSM":"SSM - Main Synoptic Hour Surface Observation",
1740 | "STA":"STA - Network and Severe Weather Statistical Summaries",
1741 | "STD":"STD - Satellite Tropical Disturbance Summary",
1742 | "STO":"STO - Road Condition Reports (State Agencies)",
1743 | "STP":"STP - State Max/Min Temperature and Precipitation Table",
1744 | "STQ":"STQ - Spot Forecast Request",
1745 | "SUM":"SUM - Space Weather Message",
1746 | "SVR":"SVR - Severe Thunderstorm Warning",
1747 | "SVS":"SVS - Severe Weather Statement",
1748 | "SWO":"SWO - Severe Storm Outlook Narrative (AC)",
1749 | "SWS":"SWS - State Weather Summary",
1750 | "SYN":"SYN - Regional Weather Synopsis",
1751 | "TAF":"TAF - Terminal Aerodrome Forecast",
1752 | "TAP":"TAP - Terminal Alerting Products",
1753 | "TAV":"TAV - Travelers Forecast Table",
1754 | "TCA":"TCA - Aviation Tropical Cyclone Advisory",
1755 | "TCD":"TCD - Tropical Cyclone Discussion",
1756 | "TCE":"TCE - Tropical Cyclone Position Estimate",
1757 | "TCM":"TCM - Marine/Aviation Tropical Cyclone Advisory",
1758 | "TCP":"TCP - Public Tropical Cyclone Advisory",
1759 | "TCS":"TCS - Satellite Tropical Cyclone Summary",
1760 | "TCU":"TCU - Tropical Cyclone Update",
1761 | "TCV":"TCV - Tropical Cyclone Watch/Warning Break Points",
1762 | "TIB":"TIB - Tsunami Bulletin",
1763 | "TID":"TID - Tide Report",
1764 | "TMA":"TMA - Tsunami Tide/Seismic Message Acknowledgement",
1765 | "TOE":"TOE - 911 Telephone Outage Emergency",
1766 | "TOR":"TOR - Tornado Warning",
1767 | "TPT":"TPT - Temperature Precipitation Table (Natl and Intnl)",
1768 | "TSU":"TSU - Tsunami Watch/Warning",
1769 | "TUV":"TUV - Weather Bulletin",
1770 | "TVL":"TVL - Travelers Forecast",
1771 | "TWB":"TWB - Transcribed Weather Broadcast",
1772 | "TWD":"TWD - Tropical Weather Discussion",
1773 | "TWO":"TWO - Tropical Weather Outlook and Summary",
1774 | "TWS":"TWS - Tropical Weather Summary",
1775 | "URN":"URN - Aircraft Reconnaissance",
1776 | "UVI":"UVI - Ultraviolet Index",
1777 | "VAA":"VAA - Volcanic Activity Advisory",
1778 | "VER":"VER - Forecast Verification Statistics",
1779 | "VFT":"VFT - Terminal Aerodrome Forecast (TAF) Verification",
1780 | "VOW":"VOW - Volcano Warning",
1781 | "WA0":"WA0 - Airmet (Pacific)",
1782 | "WA1":"WA1 - Airmet (Northeast)",
1783 | "WA2":"WA2 - Airmet (Southeast)",
1784 | "WA3":"WA3 - Airmet (North Central)",
1785 | "WA4":"WA4 - Airmet (South Central)",
1786 | "WA5":"WA5 - Airmet (Rocky Mountains)",
1787 | "WA6":"WA6 - Airmet (West Coast)",
1788 | "WA7":"WA7 - Airmet (Juneau, AK)",
1789 | "WA8":"WA8 - Airmet (Anchorage, AK)",
1790 | "WA9":"WA9 - Airmet (Fairbanks, AK)",
1791 | "WAR":"WAR - Space Environment Warning",
1792 | "WAT":"WAT - Space Environment Watch",
1793 | "WCN":"WCN - Weather Watch Clearance Notification",
1794 | "WCR":"WCR - Weekly Weather and Crop Report",
1795 | "WDA":"WDA - Weekly Data for Agriculture",
1796 | "WDU":"WDU - Warning Decision Update",
1797 | "WEK":"WEK - Routine Space Environment Product Issued Weekly",
1798 | "WOU":"WOU - Tornado/Severe Thunderstorm Watch",
1799 | "WS1":"WS1 - Sigmet (Northeast)",
1800 | "WS2":"WS2 - Sigmet (Southeast)",
1801 | "WS3":"WS3 - Sigmet (North Central)",
1802 | "WS4":"WS4 - Sigmet (South Central)",
1803 | "WS5":"WS5 - Sigmet (Rocky Mountains)",
1804 | "WS6":"WS6 - Sigmet (West Coast)",
1805 | "WST":"WST - Tropical Cyclone Sigmet",
1806 | "WSV":"WSV - Volcanic Activity Sigmet",
1807 | "WSW":"WSW - Winter Weather Warnings / Watches / Advisories",
1808 | "WWA":"WWA - Watch Status Report",
1809 | "WWP":"WWP - Severe Thunderstorm / Tornado Watch Probabilities",
1810 | "ZFP":"ZFP - Zone Forecast Product"
1811 | ]
1812 | return x
1813 | }
1814 |
1815 | // ======= Pushover Routines ============
1816 |
1817 | def send_message(msgData) {
1818 | if (sendPushEnabled) {sendPush(msgData)}
1819 | if (sendSMSEnabled) {sendSms(mobilePhone, msgData)}
1820 | if (pushoverEnabled) {sendPushoverMessage(msgData)}
1821 | }
1822 |
1823 | def sendPushoverMessage(msgData) {
1824 | if (settings.pushoverDevices != null) {
1825 | settings.pushoverDevices.each { // Use notification devices on Hubitat
1826 | it.deviceNotification(msgData)
1827 | }
1828 | }
1829 | }
1830 |
1831 | def findMyPushoverDevices() {
1832 | Boolean validated = false
1833 | List pushoverDevices = []
1834 | Map params = [
1835 | uri: "https://api.pushover.net",
1836 | path: "/1/users/validate.json",
1837 | contentType: "application/json",
1838 | requestContentType: "application/json",
1839 | body: [token: pushoverToken.trim() as String, user: pushoverUser.trim() as String] as Map
1840 | ]
1841 | try {
1842 | httpPostJson(params) { resp ->
1843 | if(resp?.status != 200) {
1844 | logErr "Received HTTP error ${resp.status}. Check your User and App Pushover keys!"
1845 | } else {
1846 | if(resp?.data) {
1847 | if(resp?.data?.status && resp?.data?.status == 1) validated = true
1848 | if(resp?.data?.devices) {
1849 | logDebug "Found (${resp?.data?.devices?.size()}) Pushover Devices..."
1850 | pushoverDevices = resp?.data?.devices
1851 | } else {
1852 | logErr "Device List is empty"
1853 | pushoverDevices ['No devices found, Check your User and App Pushover keys!']
1854 | }
1855 | } else { validated = false }
1856 | }
1857 | logDebug "findMyPushoverDevices | Validated: ${validated} | Resp | status: ${resp?.status} | data: ${resp?.data}"
1858 | }
1859 | } catch (Exception ex) {
1860 | if(ex instanceof groovyx.net.http.HttpResponseException && ex?.response) {
1861 | logErr "findMyPushoverDevices HttpResponseException | Status: (${ex?.response?.status}) | Data: ${ex?.response?.data}"
1862 | } else logErr "An invalid key was probably entered. PushOver Server Returned: ${ex}"
1863 | }
1864 | return pushoverDevices
1865 | }
1866 |
1867 | def pushoverResponse(resp, data) {
1868 | try {
1869 | Map headers = resp?.getHeaders()
1870 | def limit = headers["X-Limit-App-Limit"]
1871 | def remain = headers["X-Limit-App-Remaining"]
1872 | def resetDt = headers["X-Limit-App-Reset"]
1873 | if(resp?.status == 200) {
1874 | logDebug "Message Received by Pushover Server ${(remain && limit) ? " | Monthly Messages Remaining (${remain} of ${limit})" : ""}"
1875 | } else if (resp?.status == 429) {
1876 | logWarn "Couldn't Send Pushover Notification... You have reached your (${limit}) notification limit for the month"
1877 | } else {
1878 | if(resp?.hasError()) {
1879 | logErr "pushoverResponse: status: ${resp.status} | errorMessage: ${resp?.getErrorMessage()}"
1880 | logErr "Received HTTP error ${resp?.status}. Check your keys!"
1881 | }
1882 | }
1883 | } catch (ex) {
1884 | if(ex instanceof groovyx.net.http.HttpResponseException && ex?.response) {
1885 | def rData = (ex?.response?.data && ex?.response?.data != "") ? " | Data: ${ex?.response?.data}" : ""
1886 | logErr "pushoverResponse() HttpResponseException | Status: (${ex?.response?.status})${rData}"
1887 | } else { logErr "pushoverResponse() Exception:", ex }
1888 | }
1889 | }
--------------------------------------------------------------------------------