├── 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\nWeather 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 | } --------------------------------------------------------------------------------