/* * AmbientEcowittWeather * * Description: * This Hubitat driver polls the AmbientWeather API OR receives data from your local Ecowitt weather station. If you reuse this code * in your own driver, you must obtain your own developer API Key to place in the ApplicationKey value of the PollAmbient procedure. * The driver CAN support child devices if the WeatherSensorChild.groovy driver is also installed. This will generate a number of * children based on the sensors reporting. Children will be automatically created when applicable data is posted. All children should be * deleted if the feature is disabled (it is disabled by default). Child sensors can be useful for Rules or the Dashboard in order to * focus on particular values, like a specific temperature sensor, or combining temperature and humidity readings. * * Setup Information - Ambient API: * For the Ambient API method you will need to enter: * 1) The MAC Address of the weather station you want to poll * 2) An API Key requested from AmbientWeather to allow data to be polled * * Setup Information - Ecowitt Local * On Ecowitt Gateway app * 1) Select the Ecowitt Gateway from the Live Devices * 2) Select More * 3) Select Weather Services * 4) Select Next until Customized is shown * 5) Set the Server IP/Hostname to match your Hubitat * 6) Set the Port to be 39501 * 7) Leave the default Path as entered * 8) Set the Upload Interval as desired (faster intervals may cause excessive events on your hub) * 9) Save * On Hubitat * 1) Setup a new virtual device * Type = AmbientEcowittWeather * Device Network ID = MAC Address of your Ecowitt Gateway, all uppercase, no : or spaces in it (ex: ABCDEF012345) * 2) Save Device * 3) Set method of obtaining weather to Ecowitt Local in Preferences, as well as any other desired preferences * 4) Save Preferences * * Setup Information - Ambient Local * Follow the same steps as in the Ecowitt Local, except with settings for your Ambient station (ex: Ambient's MAC address) * * Setup Information - Live Data Scrape * 1) In the device Preferences, select the Weather Method of "Live Data Scrape", then Save Preferences * 2) Set the IP Address / Hostname of the ObserverIP device * 2) Set any other Preferences desired * 3) Save Preferences * * Features List: * Child devices generated based on sensors reporting * Error checking of Ambient MAC Address including some possible auto-correction * Calculates DewPoint from tempf and humidityout (if not reported) * Calculates HeatIndex if tempf >= 80F but reports null otherwise * Calculates WindChill if tempf <= 50F and windSpeed > 3mph but reports null otherwise * Ability to hide preferences to keep the screen simpler * Wind direction now has an alternative WindDirectionString that spells out the direction * Support for Ecowitt Weather Stations that allow local reporting from weather station * Ability to read and report all known values returned from the AmbientWeather API * Ability to check a website (mine) to notify user if there is a newer version of the driver available * * Licensing: * Copyright 2024 David Snell * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * * Version Control: * 0.8.10 - Correction to ProcessEvent function and removal of old driver-specific attributes when Preferences are saved * 0.8.9 - Changed Ambient Local method to remove extra data separator (if present) * 0.8.8 - Added soilad as a handled data point and replaced driver attributes "Driver Name" with "DriverName", "Driver Version" with "DriverVersion", and * "Driver Status" with "DriverStatus" in order to eliminate some visual glitches from attributes with spaces * 0.8.7 - Corrected wrong state name for rainrate and added runtime data provided by Ecowitt gateway * 0.8.6 - Change to UV processing rather than just a <30 value and change to how LiveData page is queried * 0.8.5 - Added additional windspeed field name recognition * 0.8.4 - Remove most instances of it.key to instead use specific variable names, cleanup of UVI (values must be <30 and only reports as uvi or ultravioletIndex) * 0.8.3 - Live Data was being attempted over HTTPS, switched to HTTP * 0.8.2 - Initial attempt at allowing scraping of Live Data from ObserverIP * 0.8.1 - Additional logging for local data methods * 0.8.0 - Correction to child device naming, added battery back in as duplicate for battout, allowing events to be "forced" for child devices * 0.7.37 - Added additional nomenclature for leak sensors * 0.7.36 - Fix for when a data field's value is returned as null * 0.7.35 - Ignoring any data fields that come back with null values * 0.7.34 - Rework of how degree symbol is used due to constant file encoding errors * 0.7.33 - Duplicate final rain values to rain sensor if it appears to be present * 0.7.32 - More accurate formula for dewPoint now in use if it is calculated (not provided by station) * 0.7.31 - Typo for separate temp/humidity sensors fixed and added a "Last Updated" attribute for the parent * 0.7.30 - Additional changes to dewPoint, HeatIndex, and WindChill calculations * 0.7.29 - Corrections to dewPoint, HeatIndex, and WindChill calculations and change lightning_day to an event * 0.7.28 - Attribute cleanup for Air Quality Sensor * 0.7.27 - Correction to pm25 AQI handling * 0.7.26 - Correction to pm25 AQI handling and addition of command to clear state variables * 0.7.25 - Handling for Air Quality Sensor AQI-reporting * 0.7.24 - Further refinements of PM25 and AQI-related areas * 0.7.23 - Removed Indoor/Outdoor selection for AQI and just perform both * 0.7.22 - Made AQI Processing able to be based on 24hr Average * 0.7.21 - Removed pm25 "Indoor" nomenclature and added Air Quality Sensor handling * 0.7.20 - Added comments for weekly/monthly values as they are calendar based not # of days * 0.7.19 - Correction for humidity being reported based on humidityout when it should have been humidityin * 0.7.18 - Separated soil sensor batteries again but corrected the Ambient Local one to be for Soil added and updated some attributes * 0.7.17 - Combined multiple data points for soil sensor batteries into 1 section and switch to newer method of update checking * 0.7.16 - Separated Soil Sensors from normal Sensors because of changes to the data returned. * 0.7.15 - Further correction to lightning distance to handle data returned as a string when it needs a number * 0.7.14 - Correction to lightning distance when in local mode * 0.7.13 - Correction to battout due to missing parent DNI, corrections to if statements for int values, added @jlv's recommendation for battery method * 0.7.12 - Fixed code to handle sensors numbered higher than 1 (broke when adding support for sensor 10) * 0.7.11 - Correction to battery values displayed when using Ambient Local mode * 0.7.10 - Added Ambient Local capability to receive data directly from Ambient stations * 0.7.9 - Additional attributes for Ambient lightning and soil sensors * 0.7.8 - Changes to test local data method from Ambient systems * 0.7.7 - Changes for better logging of Ecowitt data and updates to the driver version checking * 0.7.6 - Change ConvertEpochToDate to handle strings not numbers, handle "lightning_day" data * 0.7.5 - Edit to ConvertEpochToDate to deal with values greater than int (Epoch reported in milliseconds, not the normal seconds) * 0.7.4 - Updated instances of PM to use float values instead of int * 0.7.3 - Update to remove Soil Sensor and just have them report as Sensor. Plus change if a battout is reported but no Outdoor Station exists otherwise. * 0.7.2 - Correction for soilhum# to be shown on parent correctly, change to logging for child data * 0.7.1 - Correction to account for a data return missing a complete line * 0.7.0 - Added Air Quality Index (AQI) based on PM2.5 if someone has one, fixed lightning strike time * 0.6.9 - Change pressure from psi to inHg * 0.6.8 - Found one more instance of ProcessState being incorrectly called, rechecked ALL of them now * 0.6.7 - Fixed a couple times where ProcessState was called with a unit * 0.6.6 - Reworked RefreshRate to make it a bit more friendly and added support for WH57 (Lightning) and WH55 (Leak) sensors * 0.6.5 - Addition of battin * 0.6.4 - Corrections to totalRain and some wind attributes * 0.6.3 - Corrections to typos in windSpeed, RainRate, and lastRain attributes * 0.6.2 - Minor preferences fix * 0.6.1 - New mechanism of version control to match industry standards https://semver.org and rework state/event methods * 0.05 - Corrected temperature indoor/outdoor being written, rearranged preferences and bolded titles * 0.04 - Added Ecowitt Gateway setup instructions and altered child device naming to make them unique * 0.03 - Corrected soil sensor battery reporting (it was overlapping with normal sensors) * 0.02 - Made ALL values post to their respective child * 0.01 - Initial revision based on my AmbientWeather v0.99 driver * * Thank you(s): * Thank you to @jlv for catching a lot of things with the Ambient local method. * Thank you to @Cobra for inspiration of how I perform driver version checking. * Thank you to @christi999 for determining how Ecowitt devices could use the driver. * Thank you to @mircolino for working out a parent/child method and pointing out other areas for significant improvement. * Thank you to @bertabcd1234 for figuring out the parsing needed to handle local data from Ambient stations themselves. * Thank you to @user2597 for helping get the Air Quality Sensor data worked out and a substantial beta-testing period. */ // Returns the driver name def DriverName(){ return "AmbientEcowittWeather" } // Returns the driver version def DriverVersion(){ return "0.8.10" } // Driver Metadata metadata{ definition( name: "AmbientEcowittWeather", namespace: "Snell", author: "David Snell", importUrl: "https://www.drdsnell.com/projects/hubitat/drivers/AmbientEcowittWeather.groovy" ) { // Indicate what capabilities the device should be capable of capability "Sensor" capability "Refresh" capability "Battery" capability "PressureMeasurement" capability "RelativeHumidityMeasurement" capability "CarbonDioxideMeasurement" capability "TemperatureMeasurement" capability "UltravioletIndex" capability "IlluminanceMeasurement" capability "pHMeasurement" capability "WaterSensor" //command "DoSomething" // Test command, should be commented out before publishing command "ClearStateVariables" // Clears state variables on parent and child devices (if enabled) // Attributes for the driver itself attribute "DriverName", "string" // Identifies the driver being used for update purposes attribute "DriverVersion", "string" // Handles version for driver attribute "DriverStatus", "string" // Handles version notices for driver // Attributes not returned by a known weather station, but calculated and provided attribute "HeatIndex", "number" // // HeatIndex is only calculated if temperature outside >= 80F attribute "WindChill", "number" // WindChill is only calculated if temperature outside <= 50F and wind speed is > 3 // Arttributes for the values returned that Hubitat supports normally //attribute "temperature", "number" //attribute "humidity", "number" // Outside humidity, but converted as desired by user preferences attribute "pressure", "number" attribute "ultravioletIndex", "number" attribute "illuminance", "number" // Based on solarradiation * 126.7 attribute "windSpeed", "number" // This attribute is NOT listed in Hubitat documentation... but is used by weather dashboard tile. attribute "windDirection", "number" // This attribute is NOT listed in Hubitat documentation... but is used by weather dashboard tile. attribute "humidityout", "number" // Not a real value returned by Ambient but a duplicate of the returned humidity //attribute "battery", "number" // Not a real value returned by Ambient but a duplicate of battout // Attributes that have been created for conversion between measurement standards attribute "baromrel", "number" // Created for switching between inHg/mmHg attribute "baromabs", "number" // Created for switching between inHg/mmHg attribute "tempout", "number" // Created for switching between F/C attribute "tempin", "number" // Created for switching between F/C attribute "temp1", "number" // Created for switching between F/C attribute "temp2", "number" // Created for switching between F/C attribute "temp3", "number" // Created for switching between F/C attribute "temp4", "number" // Created for switching between F/C attribute "temp5", "number" // Created for switching between F/C attribute "temp6", "number" // Created for switching between F/C attribute "temp7", "number" // Created for switching between F/C attribute "temp8", "number" // Created for switching between F/C attribute "temp9", "number" // Created for switching between F/C attribute "temp10", "number" // Created for switching between F/C attribute "soiltemp1", "number" // Created for switching between F/C attribute "soiltemp2", "number" // Created for switching between F/C attribute "soiltemp3", "number" // Created for switching between F/C attribute "soiltemp4", "number" // Created for switching between F/C attribute "soiltemp5", "number" // Created for switching between F/C attribute "soiltemp6", "number" // Created for switching between F/C attribute "soiltemp7", "number" // Created for switching between F/C attribute "soiltemp8", "number" // Created for switching between F/C attribute "soiltemp9", "number" // Created for switching between F/C attribute "soiltemp10", "number" // Created for switching between F/C attribute "windGust", "number" // Created for switching between mph/kph attribute "windspd_avg2m", "number" // Created for switching between mph/kph attribute "windspd_avg10m", "number" // Created for switching between mph/kph attribute "hourlyRain", "number" // Created for switching between in/mm attribute "eventRain", "number" // Created for switching between in/mm attribute "dailyRain", "number" // Created for switching between in/mm attribute "weeklyRain", "number" // Created for switching between in/mm, based on calendar week (resets on Sundays) attribute "monthlyRain", "number" // Created for switching between in/mm, based on calendar month attribute "yearlyRain", "number" // Created for switching between in/mm, based on calendar month attribute "totalRain", "number" // Created for switching between in/mm attribute "RainRate", "number" // Created for switching between in/mm attribute "Last24HourRain", "number" // Created for switching between in/mm attribute "WindDirectionString", "string" // This attribute was created to use words for the direction of the wind // Attributes that exist in the data returned but MAY be modified due to measurement standards attribute "dewPoint", "number" attribute "feelsLike", "number" attribute "dewPointin", "number" attribute "feelsLikein", "number" attribute "baromrelin", "number" attribute "baromabsin", "number" attribute "windgustmph", "number" attribute "windspeedmph", "number" attribute "maxdailygust", "number" attribute "windspdmph_avg2m", "number" attribute "windspdmph_avg10m", "number" attribute "eventrainin", "number" attribute "hourlyrainin", "number" attribute "dailyrainin", "number" attribute "weeklyrainin", "number" // based on calendar week (resets on Sunday) attribute "monthlyrainin", "number" // based on calendar month attribute "yearlyrainin", "number" // based on calendar year attribute "totalrainin", "number" attribute "totalRain", "number" // Attributes for returned data attribute "tempf", "number" // Outside temperate in Fahrenheit attribute "tempinf", "number" // Inside temperate in Fahrenheit attribute "date", "string" attribute "uv", "number" attribute "winddir", "number" attribute "dateutc", "string" attribute "Last Updated", "string" attribute "solarradiation", "number" attribute "humidityin", "number" // Inside humidity attribute "battin", "number" attribute "battout", "number" attribute "windgustdir", "number" attribute "WindGustDirectionString", "string" // This attribute was created to use words for the direction of the wind attribute "winddir_avg2m", "number" attribute "winddir_avg2mString", "string" attribute "winddir_avg10m", "number" attribute "winddir_avg10mString", "string" attribute "tz", "string" attribute "lastrain", "string" attribute "lastRain", "string" attribute "humidity1", "number" attribute "humidity2", "number" attribute "humidity3", "number" attribute "humidity4", "number" attribute "humidity5", "number" attribute "humidity6", "number" attribute "humidity7", "number" attribute "humidity8", "number" attribute "humidity9", "number" attribute "humidity10", "number" attribute "temp1", "number" attribute "temp2", "number" attribute "temp3", "number" attribute "temp4", "number" attribute "temp5", "number" attribute "temp6", "number" attribute "temp7", "number" attribute "temp8", "number" attribute "temp9", "number" attribute "temp10", "number" attribute "soiltemp1", "number" attribute "soiltemp2", "number" attribute "soiltemp3", "number" attribute "soiltemp4", "number" attribute "soiltemp5", "number" attribute "soiltemp6", "number" attribute "soiltemp7", "number" attribute "soiltemp8", "number" attribute "soiltemp9", "number" attribute "soiltemp10", "number" attribute "soilhum1", "number" attribute "soilhum2", "number" attribute "soilhum3", "number" attribute "soilhum4", "number" attribute "soilhum5", "number" attribute "soilhum6", "number" attribute "soilhum7", "number" attribute "soilhum8", "number" attribute "soilhum9", "number" attribute "soilhum10", "number" attribute "soilbatt1", "number" attribute "soilbatt2", "number" attribute "soilbatt3", "number" attribute "soilbatt4", "number" attribute "soilbatt5", "number" attribute "soilbatt6", "number" attribute "soilbatt7", "number" attribute "soilbatt8", "number" attribute "soilbatt9", "number" attribute "soilbatt10", "number" attribute "soilbatt1%", "number" attribute "soilbatt2%", "number" attribute "soilbatt3%", "number" attribute "soilbatt4%", "number" attribute "soilbatt5%", "number" attribute "soilbatt6%", "number" attribute "soilbatt7%", "number" attribute "soilbatt8%", "number" attribute "soilbatt9%", "number" attribute "soilbatt10%", "number" attribute "batt1", "number" attribute "batt2", "number" attribute "batt3", "number" attribute "batt4", "number" attribute "batt5", "number" attribute "batt6", "number" attribute "batt7", "number" attribute "batt8", "number" attribute "batt9", "number" attribute "batt10", "number" attribute "batt1%", "number" attribute "batt2%", "number" attribute "batt3%", "number" attribute "batt4%", "number" attribute "batt5%", "number" attribute "batt6%", "number" attribute "batt7%", "number" attribute "batt8%", "number" attribute "batt9%", "number" attribute "batt10%", "number" attribute "relay1", "number" attribute "relay2", "number" attribute "relay3", "number" attribute "relay4", "number" attribute "relay5", "number" attribute "relay6", "number" attribute "relay7", "number" attribute "relay8", "number" attribute "relay9", "number" attribute "relay10", "number" attribute "'24hourrainin'", "number" attribute "dewPoint1", "number" attribute "dewPoint2", "number" attribute "dewPoint3", "number" attribute "dewPoint4", "number" attribute "dewPoint5", "number" attribute "dewPoint6", "number" attribute "dewPoint7", "number" attribute "dewPoint8", "number" attribute "dewPoint9", "number" attribute "dewPoint10", "number" attribute "feelsLike1", "number" attribute "feelsLike2", "number" attribute "feelsLike3", "number" attribute "feelsLike4", "number" attribute "feelsLike5", "number" attribute "feelsLike6", "number" attribute "feelsLike7", "number" attribute "feelsLike8", "number" attribute "feelsLike9", "number" attribute "feelsLike10", "number" attribute "batt_co2", "number" // Ecowitt specific attributes attribute "stationtype", "string" attribute "pm25_ch1", "number" attribute "pm25_avg_24h_ch1", "number" attribute "pm25batt1", "number" attribute "pm25_ch2", "number" attribute "pm25_avg_24h_ch2", "number" attribute "pm25batt2", "number" attribute "pm25_ch3", "number" attribute "pm25_avg_24h_ch3", "number" attribute "pm25batt3", "number" attribute "pm25_ch4", "number" attribute "pm25_avg_24h_ch4", "number" attribute "pm25batt4", "number" attribute "freq", "string" attribute "model", "string" // WH57 Lightning sensor attribute "lightning", "string" attribute "lightning_time", "string" attribute "lightning_num", "number" attribute "lightning_day", "string" attribute "lightning_hour", "string" attribute "batt_lightning", "number" attribute "wh57batt", "number" attribute "Last Lightning Strike Time", "string" attribute "Lightning Strike Distance", "number" attribute "Lightning Strike Count", "number" // WH55 Leak sensor attribute "leak_ch1", "number" // 0 = no leak, 1 = leak attribute "leak_ch2", "number" // 0 = no leak, 1 = leak attribute "leak_ch3", "number" // 0 = no leak, 1 = leak attribute "leak_ch4", "number" // 0 = no leak, 1 = leak attribute "leak_ch1_water", "enum", [ "wet", "dry" ] attribute "leak_ch2_water", "enum", [ "wet", "dry" ] attribute "leak_ch3_water", "enum", [ "wet", "dry" ] attribute "leak_ch4_water", "enum", [ "wet", "dry" ] attribute "leakbatt1", "number" // 5 = full battery attribute "leakbatt2", "number" // 5 = full battery attribute "leakbatt3", "number" // 5 = full battery attribute "leakbatt4", "number" // 5 = full battery // Air Quality Index attribute "Region", "string" attribute "co2", "number" attribute "pm25", "number" attribute "pm25_24hr", "number" attribute "pm25_avg_24hr", "number" attribute "pm25_in", "number" attribute "pm25_in_24hr", "number" attribute "pm25_in_avg_24hr", "number" attribute "batt_25", "number" attribute "Air Quality Index", "number" attribute "Air Quality Index 24hr Average", "number" attribute "Indoor Air Quality Index 24hr Average", "number" attribute "Indoor Air Quality Index", "number" attribute "Air Quality Index 1", "number" attribute "Air Quality Index 2", "number" attribute "Air Quality Index 3", "number" attribute "Air Quality Index 4", "number" attribute "Air Quality Index String", "string" attribute "Air Quality Index 24hr Average String", "string" attribute "Indoor Air Quality Index String", "string" attribute "Indoor Air Quality Index 24hr Average String", "string" attribute "Air Quality Index 1 String", "string" attribute "Air Quality Index 2 String", "string" attribute "Air Quality Index 3 String", "string" attribute "Air Quality Index 4 String", "string" attribute "Air Quality Index Color", "string" attribute "Air Quality Index 24hr Average Color", "string" attribute "Indoor Air Quality Index Color", "string" attribute "Indoor Air Quality Index 24hr Average Color", "string" attribute "Air Quality Index 1 Color", "string" attribute "Air Quality Index 2 Color", "string" attribute "Air Quality Index 3 Color", "string" attribute "Air Quality Index 4 Color", "string" attribute "aqi_pm25", "number" attribute "aqi_pm25_aqin", "number" attribute "aqi_pm25_24hr", "number" attribute "Indoor pm10", "number" attribute "Indoor pm10_24hr", "number" attribute "Indoor pm25", "number" attribute "Indoor pm25_24hr", "number" attribute "Indoor co2", "number" attribute "Indoor co2_24hr", "number" attribute "Air Quality Sensor pm25", "number" attribute "Air Quality Sensor pm25_24hr", "number" attribute "Air Quality Sensor Temperature", "number" attribute "Air Quality Sensor Humidity", "number" attribute "Air Quality Sensor Indoor pm10", "number" attribute "Air Quality Sensor Indoor pm10_24hr", "number" attribute "Air Quality Sensor Indoor pm25", "number" attribute "Air Quality Sensor Indoor pm25_24hr", "number" attribute "Air Quality Sensor Indoor co2", "number" attribute "Air Quality Sensor Indoor co2_24hr", "number" attribute "Air Quality Sensor Battery", "number" } preferences{ section{ if( ShowAllPreferences || ShowAllPreferences == null ){ // Show the preferences options if( DataMethod != null ){ input( type: "bool", name: "OutdoorTemperature", title: "Report outdoor temperature?", description: "Turning this off will have events report the indoor temperature.", required: false, defaultValue: true ) input( type: "bool", name: "OutdoorHumidity", title: "Report outdoor humidity?", description: "Turning this off will have events report the indoor humidity.", required: false, defaultValue: true ) input( type: "enum", name: "WindDirMethod", title: "Wind Direction Method?", description: "How do you want wind direction indicated?", required: false, defaultValue: "1", multiple: false, options: [ [ "1" : "Degrees Only" ], [ "2" : "4 Compass Values (Letter Only)" ], [ "3" : "8 Compass Values (Letters Only)" ], [ "4" : "16 Compass Values (Letters Only)" ], [ "5" : "4 Compass Values (Words)" ], [ "6" : "8 Compass Values (Words)" ], [ "7" : "16 Compass Values (Words)" ] ] ) input( type: "enum", name: "MeasurementStandard", title: "Measurement Standard?", description: "Metric (as in kilometers) or Imperial (as in miles)", required: false, defaultValue: "Imperial", multiple: false, options: [ [ "Metric" : "Metric" ], [ "Imperial" : "Imperial" ] ] ) input( type: "bool", name: "ChildrenEnabled", title: "Enable Child Devices?", description: "Once enabled, child devices will be made for added sensors.", required: false, defaultValue: false ) } if( DataMethod == "Ambient API" ){ input( type: "string", name: "APIKey", title: "Personal API Key for AmbientWeather", required: true ) input( type: "string", name: "MACAddress", title: "MAC Address of station", description: "xx:xx:xx:xx:xx:xx format", required: true ) input( type: "enum", name: "RefreshRate", title: "Refresh Rate", required: false, multiple: false, options: [ "1 minute", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", "3 hours", "Manual" ], defaultValue: "5 minutes" ) } if( DataMethod == "Live Data Scrape" ){ input( type: "string", name: "IPAddress", title: "IP Address / Hostname of ObserverIP", required: true ) input( type: "enum", name: "RefreshRate", title: "Refresh Rate", required: false, multiple: false, options: [ "1 minute", "5 minutes", "10 minutes", "15 minutes", "30 minutes", "1 hour", "3 hours", "Manual" ], defaultValue: "5 minutes" ) } input( type: "enum", name: "Region", title: "Region", description: "Used for Air Quality Index (AQI)", required: false, options: [ "Australia", "Europe (CAQI)", "India", "Mainland China", "United Kingdom", "United States" ] ) //input( type: "enum", name: "AQISensor", title: "AQI Using Indoor or Outdoor Values?", description: "Used for Air Quality Index (AQI)", required: false, defaultValue: "Indoor", options: [ "Indoor", "Outdoor" ] ) input( type: "enum", name: "DataMethod", title: "Weather Method?", description: "Which method of obtaining weather data do you want to use?", required: true, multiple: false, options: [ "Ambient API", "Ecowitt Local", "Ambient Local", "Live Data Scrape" ] ) input( type: "enum", name: "LogType", title: "Enable Logging?", required: true, multiple: false, options: [ "None", "Info", "Debug", "Trace" ], defaultValue: "Info" ) input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) } else { input( type: "bool", name: "ShowAllPreferences", title: "Show All Preferences?", defaultValue: true ) } } } } // This command is meant for development/testing purposes only and should not be able to be run directly in published versions def DoSomething(){ } // updated is called whenever device parameters are saved def updated(){ /* * Check what the refresh rate is set for. Ambient Weather has a MAXIMUM refresh rate of 1 per second. * I set a maximum refresh rate of 1 per minute... If you need more, get a different Application Key and rework this. * In practice, the API will only have data available over particular blocks of time so repeatedly polling the API * will likely just return the same data with the same original date information for when it was posted to the API. */ // Set the schedule for driver version check and refreshing for data unschedule() if( ( DataMethod == "Ambient API" ) || ( DataMethod == null ) ){ if( RefreshRate == null ){ RefreshRate = "5 minutes" } // Check what the refresh rate is set for then run it switch( RefreshRate ){ case "1 minute": runEvery1Minute( refresh ) break case "5 minutes": runEvery5Minutes( refresh ) break case "10 minutes": runEvery10Minutes( refresh ) break case "15 minutes": runEvery15Minutes( refresh ) break case "30 minutes": runEvery30Minutes( refresh ) break case "1 hour": runEvery1Hour( refresh ) break case "3 hours": runEvery3Hours( refresh ) break case "Manual": break } Logging( "Refresh rate: ${ RefreshRate }", 3 ) } // Set the driver name and version before update checking is scheduled ProcessEvent( "DriverName", DriverName() ) ProcessEvent( "DriverVersion", DriverVersion() ) // Schedule the daily driver version check schedule( new Date(), CheckForUpdate ) if( !ChildrenEnabled ){ getChildDevices().each{ Logging( "Children disabled, deleting ${ it.deviceNetworkId }", 3 ) deleteChildDevice( it.deviceNetworkId ) } } Logging( "Updated", 2 ) } // refresh performs a poll of data def refresh(){ // Clear current state values state.clear() if( state."Driver Name" != null ){ state.remove( "Driver Name" ) state.remove( "Driver Version" ) device.deleteCurrentState( "Driver Name" ) device.deleteCurrentState( "Driver Version" ) } ProcessState( "DriverName", "${ DriverName() }" ) ProcessState( "DriverVersion", "${ DriverVersion() }" ) if( MeasurementStandard == null ){ MeasurementStandard = "Imperial" } if( WindDirMethod == null ){ WindDirMethod = "1" } if( LogType == null ){ LogType = "2" } if( DataMethod == "Ambient API" ){ if( MACAddress != null && APIKey != null ){ PollAmbient() } else { Logging( "MAC Address & APIKey are required for Ambient Weather API.", 5 ) } } else if( DataMethod == "Live Data Scrape" ){ if( IPAddress != null ){ PollLiveData() } else { Logging( "IP Address /Hostname is required for Live Data Scrape method.", 5 ) } } } // Clears state variables for the parent device and any children def ClearStateVariables(){ state.clear() if( ChildrenEnabled ){ getChildDevices().each{ getChildDevice( it.deviceNetworkId ).ClearStateVariables() } } } // installed is called when the device is installed, all it really does is run updated def installed(){ Logging( "Installed", 2 ) updated() } // initialize is called when the device is initialized, all it really does is run updated def initialize(){ Logging( "Initialized", 2 ) updated() } // uninstalling device so make sure to clean up children void uninstalled() { // Delete all children getChildDevices().each{ deleteChildDevice( it.deviceNetworkId ) } Logging( "Uninstalled", 2 ) } // parses parameters received. This code is from @bertabcd1234 Map parseURLParameters(String parameters) { if( parameters.startsWith( "&" ) ){ parameters = parameters.minus( "&" ) } List keysAndVals = parameters.contains("&") ? parameters.split("&") : [] Map data = keysAndVals.collectEntries( { [(URLDecoder.decode(it.split("=")[0])): URLDecoder.decode(it.split("=")[1])] }) return data } // parse appears to be one of those "special" methods for when data is returned // but I wanted to make sure that receiving Ecowitt data was being handled on // it's own and could be more readily reworked if needed // Code for Ambient Local copied (with permission) from @bertabcd1234 def parse( String description ){ Logging( "Raw data = ${ description }", 4 ) if( DataMethod == "Ecowitt Local" ){ ReceiveEcowitt( description ) } else if( DataMethod == "Ambient Local" ){ Map data = [:] // So for Ambient Weather, instead do manual parsing in a few steps: // 1. Extract (encoded) "headers:" from LAN message: tokenize, find "headers:", return this portion // as (still-encoded) String: String headers = description.split( "," )?.collect({ it.trim() } )?.find( { it.startsWith ( "headers:" ) } ).substring( 8 ) Logging( "Ambient Local Headers = ${ headers }", 4 ) // 2. Extract full URI from GET (bit hacky but should work for this data)--take second whitespace-separated // part of raw string after decoding: String rawData = new String( URLDecoder.decode( headers ).decodeBase64(), "UTF-8" ).split( "\\s+" )[ 1 ] Logging( "Ambient Local rawData Step 1 = ${ rawData }", 4 ) // 3. Strip off path (must end with "/" as default does--device also seems to also let you configure without, // which won't work (here or anywhere): rawData = rawData.substring( rawData.lastIndexOf( "/" ) + 1 ) Logging( "Ambient Local rawData Step 2 = ${ rawData }", 4 ) // 4. Should now be "&"-separated list of "key=value" items, so split into Map and we're done! data = parseURLParameters( rawData ) PostWeather( data ) } } // Receive Ecowitt station for data without needing to access an API. Original concept from @christi999, replaced by @mircolino's design def ReceiveEcowitt( String description ){ def msg = parseLanMessage( description ) Logging( "Received Ecowitt = ${ msg }", 4 ) def body = msg.body // => request body as a string if( body != null ){ Logging( "Received: ${ body }", 4 ) // Building Map from string Map Data = [:] body.split( "&" ).each{ String[] keyValue = it.split( "=" ) if( keyValue.size() > 1 ){ Data[ keyValue[ 0 ] ] = keyValue[ 1 ] } else { Data[ keyValue[ 0 ] ] = null } } Logging( "Ecowitt Mapping = ${ Data }", 4 ) PostWeather( Data ) } else { Logging( "No data received in Ecowitt response.", 4 ) } } //Poll AmbientWeather for data def PollAmbient(){ // If you are going to reuse this code, you need to get your own ApplicationKey from AmbientWeather // I have set this driver to only poll for the most recent data received from the weather station // If you need more results, request your own ApplicationKey and rework the driver def ApplicationKey = "c0d1959778684f11af086ffc4a15e5d94cfa1e53a9f1476aab6306d461245d54" // Error checking MAC Address provided def tempMAC = MACAddress tempMAC = tempMAC.replaceAll( "[.,; ]", ":" ) if( tempMAC.size() != 17 ){ if( tempMAC.size() > 17 ){ Logging( "MAC Address too long, please enter correctly in Preferences.", 5 ) } else if( tempMAC.size() > 12 && tempMAC.size() < 17 ){ Logging( "Maybe some (but not enough) separators in MAC Address, attempting to correct.", 3 ) tempMAC = tempMAC.replaceAll( "[.,;: ]", "" ) tempMAC = tempMAC.substring( 0, 2 ) + ":" + tempMAC.substring( 2, 4 ) + ":" + tempMAC.substring( 4, 6 ) + ":" + tempMAC.substring( 6, 8 ) + ":" + tempMAC.substring( 8, 10 ) + ":" + tempMAC.substring( 10, 12 ) } else if( tempMAC.size() == 12 ){ Logging( "Likely no separators in MAC Address, attempting to correct.", 4 ) tempMAC = tempMAC.substring( 0, 2 ) + ":" + tempMAC.substring( 2, 4 ) + ":" + tempMAC.substring( 4, 6 ) + ":" + tempMAC.substring( 6, 8 ) + ":" + tempMAC.substring( 8, 10 ) + ":" + tempMAC.substring( 10, 12 ) } else if( tempMAC.size() < 12 ){ Logging( "MAC Address too short, please enter correctly in Preferences.", 5 ) } } if( tempMAC.matches( "[ 0-9A-Fa-f ][ 0-9A-Fa-f ]:[ 0-9A-Fa-f ][ 0-9A-Fa-f ]:[ 0-9A-Fa-f ][ 0-9A-Fa-f ]:[ 0-9A-Fa-f ][ 0-9A-Fa-f ]:[ 0-9A-Fa-f ][ 0-9A-Fa-f ]:[ 0-9A-Fa-f ][ 0-9A-Fa-f ]" ) ){ // Checks and possible corrections passed, attempt to get the data Logging( "Getting data from MAC Address: ${ tempMAC }", 4 ) def Params = [ uri: "https://api.ambientweather.net/v1/devices/${ tempMAC }?apiKey=${ APIKey }&applicationKey=${ ApplicationKey }&endDate=&limit=1", contentType: "application/json" ] asynchttpGet( "GetAmbientWeather", Params) } else { Logging( "Invalid MAC Address saved, cannot poll.", 5 ) } } // Handles the response from AmbientWeather def GetAmbientWeather( resp, data ){ switch( resp.getStatus() ){ case 200: Logging( "Raw Data = ${ resp.data }", 4 ) if( resp.data != null ){ Map Data = [:] Temp = resp.data as String Temp.split( "," ).each{ Logging( "it = ${ it }", 4 ) Name = it.split( '"' ) if( Name.size() > 1 ){ if( Name[ 1 ] != null ){ Name2 = Name[ 1 ] Value = it.split( ":" ) Logging( "Value = ${ Value }", 4 ) switch( Name2 ){ case "date": case "lastRain": Value2 = "${ Value[ 1 ] }:${ Value[ 2 ] }:${ Value[ 3 ] }" Value = Value2.split( '"' ) break case "loc": Value2 = "${ Value[ 1 ] }" Value = Value2.split( '"' ) break } Value2 = Value[ 1 ] Logging( "Name2 = ${ Name2 } and Value2 = ${ Value2 }", 4 ) Data[ Name2 ] = Value2 } } } PostWeather( Data ) } else { Logging( "No data returned by Ambient", 5 ) } break case 429: Logging( "Poll rate on Ambient API exceeded", 5 ) case 404: default: Logging( "Error connecting to AmbientWeather: ${ resp.status }", 5 ) break } } // Scrape ObserverIP for Live Data def PollLiveData(){ // Checks and possible corrections passed, attempt to get the data Logging( "Getting data from: ${ IPAddress }", 4 ) def Params = [ uri: "http://${ IPAddress }/livedata.htm" ] asynchttpGet( "ProcessLiveData", Params) } // Handles the response of Live Data def ProcessLiveData( resp, data ){ switch( resp.getStatus() ){ case 200: Logging( "Raw Data = ${ resp.data as String }", 4 ) if( resp.data != null ){ Map Data = [:] Temp = resp.data as String Temp = Temp.split( '= 80F if( ( state.tempf >= 80 ) && ( state.humidityout != null ) ){ def T = state.tempf def RH = state.humidityout def HI = -42.379 + ( 2.04901523 * T ) + ( 10.14333127 * RH ) - ( 0.22475541 * ( T * RH ) ) - ( 0.00683783 * ( T ** 2 ) ) - ( 0.05481717 * ( RH ** 2 ) ) + ( 0.00122874 * ( ( T ** 2 ) * RH ) ) + ( 0.00085282 * ( T * ( RH ** 2 ) ) ) - ( 0.00000199 * ( ( T ** 2 ) * ( RH ** 2 ) ) ) ProcessEvent( "HeatIndex ", ConvertTemperature( "F", HI ), ReturnDegree( "Location" ) ) PostEventToChild( "Outdoor Station", "HeatIndex", ConvertTemperature( "F", HI ), ReturnDegree( "Location" ) ) } else { ProcessEvent( "HeatIndex ", null ) PostEventToChild( "Outdoor Station", "HeatIndex", null ) } // WindChill is only calculated if temperature outside <= 50F and wind speed is > 3 if( ( state.tempf <= 50 ) && ( state.windSpeed > 3 ) ){ def T = state.tempf def WS = state.windSpeed def WC = 35.74 + ( 0.6215 * T ) - ( 35.75 * ( WS ** 0.16 ) ) + ( ( 0.4275 * T ) * ( WS ** 0.16 ) ) ProcessEvent( "WindChill", ConvertTemperature( "F", WC ), ReturnDegree( "Location" ) ) PostEventToChild( "Outdoor Station", "WindChill", ConvertTemperature( "F", WC ), ReturnDegree( "Location" ) ) } else { ProcessEvent( "WindChill", null ) PostEventToChild( "Outdoor Station", "WindChill", null ) } // If a rain sensor exists, duplicate rain-related final values to it if( state.battrain != null ){ if( MeasurementStandard == "Metric" ){ PostEventToChild( "Rain Sensor", "Last24HourRain", state.Last24HourRain, "mm" ) PostEventToChild( "Rain Sensor", "eventRain", state.eventRain, "mm" ) PostEventToChild( "Rain Sensor", "RainRate", state.RainRate, "mm" ) PostEventToChild( "Rain Sensor", "hourlyRain", state.hourlyRain, "mm" ) PostEventToChild( "Rain Sensor", "dailyRain", state.dailyRain, "mm" ) PostEventToChild( "Rain Sensor", "weeklyRain", state.weeklyRain, "mm" ) PostEventToChild( "Rain Sensor", "monthlyRain", state.monthlyRain, "mm" ) PostEventToChild( "Rain Sensor", "yearlyRain", state.yearlyRain, "mm" ) PostEventToChild( "Rain Sensor", "totalRain", state.totalRain, "mm" ) } else { PostEventToChild( "Rain Sensor", "Last24HourRain", state.Last24HourRain, "in" ) PostEventToChild( "Rain Sensor", "eventRain", state.eventRain, "in" ) PostEventToChild( "Rain Sensor", "RainRate", state.RainRate, "in" ) PostEventToChild( "Rain Sensor", "hourlyRain", state.hourlyRain, "in" ) PostEventToChild( "Rain Sensor", "dailyRain", state.dailyRain, "in" ) PostEventToChild( "Rain Sensor", "weeklyRain", state.weeklyRain, "in" ) PostEventToChild( "Rain Sensor", "monthlyRain", state.monthlyRain, "in" ) PostEventToChild( "Rain Sensor", "yearlyRain", state.yearlyRain, "in" ) PostEventToChild( "Rain Sensor", "totalRain", state.totalRain, "in" ) } PostEventToChild( "Outdoor Station", "lastRain", state.lastRain ) } } // Makes a string that describes the current wind direction in words (or at least letters) def String MakeWindDirectionString( Direction ){ def WindDirectionString switch( WindDirMethod ){ case "1": // Just uses the degree value WindDirectionString = "${ Direction }${ ReturnDegree( null ) }" break case "2": // 4 Compass Points - Letters if( Direction > 315 || Direction <= 45 ){ WindDirectionString = "N" } else if( Direction > 45 && Direction <= 135 ){ WindDirectionString = "E" } else if( Direction > 135 && Direction <= 225 ){ WindDirectionString = "S" } else if( Direction > 225 && Direction <= 315 ){ WindDirectionString = "W" } break case "3": // 8 Compass Points - Letters if( Direction > 337.5 || Direction <= 22.5 ){ WindDirectionString = "N" } else if( Direction > 22.5 && Direction <= 67.5 ){ WindDirectionString = "NE" } else if( Direction > 67.5 && Direction <= 112.5 ){ WindDirectionString = "E" } else if( Direction > 112.5 && Direction <= 157.5 ){ WindDirectionString = "SE" } else if( Direction > 157.5 && Direction <= 202.5 ){ WindDirectionString = "S" } else if( Direction > 202.5 && Direction <= 247.5 ){ WindDirectionString = "SW" } else if( Direction > 247.5 && Direction <= 292.5 ){ WindDirectionString = "W" } else if( Direction > 292.5 && Direction <= 337.5 ){ WindDirectionString = "NW" } break case "4": // 16 Compass Points - Letters if( Direction > 348.75 || Direction <= 11.25 ){ WindDirectionString = "N" } else if( Direction > 11.25 && Direction <= 33.75 ){ WindDirectionString = "NNE" } else if( Direction > 33.75 && Direction <= 56.25 ){ WindDirectionString = "NE" } else if( Direction > 56.25 && Direction <= 78.75 ){ WindDirectionString = "ENE" } else if( Direction > 78.75 && Direction <= 101.25 ){ WindDirectionString = "E" } else if( Direction > 101.25 && Direction <= 123.75 ){ WindDirectionString = "ESE" } else if( Direction > 123.75 && Direction <= 146.25 ){ WindDirectionString = "SE" } else if( Direction > 146.25 && Direction <= 168.75 ){ WindDirectionString = "SSE" } else if( Direction > 168.75 && Direction <= 191.25 ){ WindDirectionString = "S" } else if( Direction > 191.25 && Direction <= 213.75 ){ WindDirectionString = "SSW" } else if( Direction > 213.75 && Direction <= 236.25 ){ WindDirectionString = "SW" } else if( Direction > 236.25 && Direction <= 258.75 ){ WindDirectionString = "WSW" } else if( Direction > 258.75 && Direction <= 281.25 ){ WindDirectionString = "W" } else if( Direction > 281.25 && Direction <= 303.75 ){ WindDirectionString = "WNW" } else if( Direction > 303.75 && Direction <= 326.25 ){ WindDirectionString = "NW" } else if( Direction > 326.25 && Direction <= 348.75 ){ WindDirectionString = "NNW" } break case "5": // 4 Compass Points - Words if( Direction > 315 || Direction <= 45 ){ WindDirectionString = "North" } else if( Direction > 45 && Direction <= 135 ){ WindDirectionString = "East" } else if( Direction > 135 && Direction <= 225 ){ WindDirectionString = "South" } else if( Direction > 225 && Direction <= 315 ){ WindDirectionString = "West" } break case "6": // 8 Compass Points - Words if( Direction > 337.5 || Direction <= 22.5 ){ WindDirectionString = "North" } else if( Direction > 22.5 && Direction <= 67.5 ){ WindDirectionString = "North-East" } else if( Direction > 67.5 && Direction <= 112.5 ){ WindDirectionString = "East" } else if( Direction > 112.5 && Direction <= 157.5 ){ WindDirectionString = "South-East" } else if( Direction > 157.5 && Direction <= 202.5 ){ WindDirectionString = "South" } else if( Direction > 202.5 && Direction <= 247.5 ){ WindDirectionString = "South-West" } else if( Direction > 247.5 && Direction <= 292.5 ){ WindDirectionString = "West" } else if( Direction > 292.5 && Direction <= 337.5 ){ WindDirectionString = "North-West" } break case "7": // 16 Compass Points - Words if( Direction > 348.75 || Direction <= 11.25 ){ WindDirectionString = "North" } else if( Direction > 11.25 && Direction <= 33.75 ){ WindDirectionString = "North-North-East" } else if( Direction > 33.75 && Direction <= 56.25 ){ WindDirectionString = "North-East" } else if( Direction > 56.25 && Direction <= 78.75 ){ WindDirectionString = "East-North-East" } else if( Direction > 78.75 && Direction <= 101.25 ){ WindDirectionString = "East" } else if( Direction > 101.25 && Direction <= 123.75 ){ WindDirectionString = "East-South-East" } else if( Direction > 123.75 && Direction <= 146.25 ){ WindDirectionString = "South-East" } else if( Direction > 146.25 && Direction <= 168.75 ){ WindDirectionString = "South-South-East" } else if( Direction > 168.75 && Direction <= 191.25 ){ WindDirectionString = "South" } else if( Direction > 191.25 && Direction <= 213.75 ){ WindDirectionString = "South-South-West" } else if( Direction > 213.75 && Direction <= 236.25 ){ WindDirectionString = "South-West" } else if( Direction > 236.25 && Direction <= 258.75 ){ WindDirectionString = "West-South-West" } else if( Direction > 258.75 && Direction <= 281.25 ){ WindDirectionString = "West" } else if( Direction > 281.25 && Direction <= 303.75 ){ WindDirectionString = "West-North-West" } else if( Direction > 303.75 && Direction <= 326.25 ){ WindDirectionString = "North-West" } else if( Direction > 326.25 && Direction <= 348.75 ){ WindDirectionString = "North-North-West" } break } return WindDirectionString } // Used to convert epoch values to text dates def String ConvertEpochToDate( String Epoch ){ Long Temp = Epoch.toLong() def date if( Temp <= 9999999999 ){ date = new Date( ( Temp * 1000 ) ).toString() } else { date = new Date( Temp ).toString() } //def date = use( groovy.time.TimeCategory ){ // new Date( 0 ) + Temp //} return date } // Checks the location.getTemperatureScale() to convert temperature values def ConvertTemperature( String Scale, Number Value ){ if( Value != null ){ def ReturnValue = Value as double if( location.getTemperatureScale() == "C" && Scale.toUpperCase() == "F" ){ ReturnValue = ( ( ( Value - 32 ) * 5 ) / 9 ) } else if( location.getTemperatureScale() == "F" && Scale.toUpperCase() == "C" ) { ReturnValue = ( ( ( Value * 9 ) / 5 ) + 32 ) } else if( location.getTemperatureScale() == Scale.toUpperCase() ){ ReturnValue = Value } def TempInt = ( ReturnValue * 100 ) as int ReturnValue = ( TempInt / 100 ) return ReturnValue } } // Converts speed between m/s and mph def ConvertMetersSecond( String BaseScale, Number Value ){ if( Value != null ){ def ReturnValue = Value as double if( BaseScale == "Metric" && MeasurementStandard == "Imperial" ){ ReturnValue = ( Value * 2.237 ) } else if( BaseScale.toUpperCase() == "Imperial" && MeasurementStandard == "Metric" ) { ReturnValue = ( Value / 2.237 ) } def TempInt = ( ReturnValue * 100 ) as int ReturnValue = ( TempInt / 100 ) return ReturnValue } } // Converts distances between km and miles or kph and mph def ConvertMiles( String BaseScale, Number Value ){ if( Value != null ){ def ReturnValue = Value as double if( BaseScale == "Metric" && MeasurementStandard == "Imperial" ){ ReturnValue = ( Value / 1.609 ) } else if( BaseScale.toUpperCase() == "Imperial" && MeasurementStandard == "Metric" ) { ReturnValue = ( Value * 1.609 ) } def TempInt = ( ReturnValue * 100 ) as int ReturnValue = ( TempInt / 100 ) return ReturnValue } } // Converts measurements between inches and mm def ConvertInches( String BaseScale, Number Value ){ if( Value != null ){ def ReturnValue = Value as double if( BaseScale == "Metric" && MeasurementStandard == "Imperial" ){ ReturnValue = ( Value / 25.4 ) } else if( BaseScale.toUpperCase() == "Imperial" && MeasurementStandard == "Metric" ) { ReturnValue = ( Value * 25.4 ) } def TempInt = ( ReturnValue * 100 ) as int ReturnValue = ( TempInt / 100 ) return ReturnValue } } // Converts pressure between mbar and inHg def ConvertPressure( String BaseScale, Number Value ){ if( Value != null ){ def ReturnValue = Value as double if( BaseScale == "Metric" && MeasurementStandard == "Imperial" ){ ReturnValue = ( Value * 0.0295301 ) } else if( BaseScale.toUpperCase() == "Imperial" && MeasurementStandard == "Metric" ) { ReturnValue = ( Value * 33.8637526 ) } def TempInt = ( ReturnValue * 100 ) as int ReturnValue = ( TempInt / 100 ) return ReturnValue } } // Ambient Local method introduced a different value for batteries where 1 = low/bad and 0 = good // This was recommended by @jlv to simplify the code overall def BatteryValue( Number Value ){ if( Value != null ){ def ReturnValue = Value as int if( DataMethod != "Ambient Local" ){ if( Value == 1 ){ ReturnValue = 0 } else { ReturnValue = 75 } } else { // Rely on the standard Ambient method of battery values if( Value == 1 ){ ReturnValue = 75 } else { ReturnValue = 0 } } return ReturnValue } } // Calculates AQI based on the PM2.5 value provided, trying to account user's selected region def ProcessAQI( Number Value, String Sensor = null, Boolean Average = false, String SensorNumber = null ){ if( Value != null ){ def AQI = 0 as int def AQIString def AQIColor def MinPM25 = 0 as float def MaxPM25 = 0 as float def MinAQI = 0 as float def MaxAQI = 0 as float def Handled = false switch( Region ){ // Canada case "Canada": break // Hong Kong case "Hong_Kong": break // Mainland China case "Mainland China": switch( Value ){ case { it <= 35 }: MinPM25 = 0 MaxPM25 = 35 MinAQI = 0 MaxAQI = 50 AQIString = "Excellent" AQIColor = "Green" break case { it > 35 && it <= 75 }: MinPM25 = 35 MaxPM25 = 75 MinAQI = 51 MaxAQI = 100 AQIString = "Good" AQIColor = "Yellow" break case { it > 75 && it <= 115 }: MinPM25 = 75 MaxPM25 = 115 MinAQI = 101 MaxAQI = 150 AQIString = "Lightly Polluted" AQIColor = "Orange" break case { it > 115 && it <= 150 }: MinPM25 = 115 MaxPM25 = 150 MinAQI = 151 MaxAQI = 200 AQIString = "Moderately Polluted" AQIColor = "Red" break case { it > 150 && it <= 250 }: MinPM25 = 150 MaxPM25 = 250 MinAQI = 201 MaxAQI = 300 AQIString = "Heavily Polluted" AQIColor = "Purple" break case { it > 250 }: MinPM25 = 250 MaxPM25 = 500 MinAQI = 301 MaxAQI = 500 AQIString = "Severely Polluted" AQIColor = "Deep Red" break } break Handled = true // India case "India": switch( Value ){ case { it <= 30 }: MinPM25 = 0 MaxPM25 = 30 MinAQI = 0 MaxAQI = 50 AQIString = "Good" AQIColor = "Green" break case { it > 30 && it <= 60 }: MinPM25 = 31 MaxPM25 = 60 MinAQI = 51 MaxAQI = 100 AQIString = "Satisfactory" AQIColor = "Green" break case { it > 60 && it <= 90 }: MinPM25 = 61 MaxPM25 = 90 MinAQI = 101 MaxAQI = 200 AQIString = "Moderately polluted" AQIColor = "Yellow" break case { it > 90 && it <= 120 }: MinPM25 = 91 MaxPM25 = 120 MinAQI = 201 MaxAQI = 300 AQIString = "Poor" AQIColor = "Orange" break case { it > 120 && it <= 250 }: MinPM25 = 121 MaxPM25 = 250 MinAQI = 301 MaxAQI = 400 AQIString = "Very poor" AQIColor = "Red" break case { it > 250 }: MinPM25 = 250 MaxPM25 = 350 MinAQI = 401 MaxAQI = 500 AQIString = "Severe" AQIColor = "Deep Red" break } //AQI = Handled = true break // Mexico case "Mexico": break // Singapore case "Singapore": break // South Korea case "South Korea": break // United Kingdom case "United Kingdom": Value = Value as int switch( Value ){ case { it <= 11 }: MinPM25 = 0 MaxPM25 = 11 AQI = 1 AQIString = "Low" AQIColor = "Green" break case { it > 12 && it <= 23 }: MinPM25 = 12 MaxPM25 = 23 AQI = 2 AQIString = "Low" AQIColor = "Green" break case { it > 24 && it <= 35 }: MinPM25 = 24 MaxPM25 = 35 AQI = 3 AQIString = "Low" AQIColor = "Green" break case { it > 36 && it <= 41 }: MinPM25 = 36 MaxPM25 = 41 AQI = 4 AQIString = "Moderate" AQIColor = "Gold" break case { it > 42 && it <= 47 }: MinPM25 = 42 MaxPM25 = 47 AQI = 5 AQIString = "Moderate" AQIColor = "Gold" break case { it > 48 && it <= 53 }: MinPM25 = 48 MaxPM25 = 53 AQI = 6 AQIString = "Moderate" AQIColor = "Gold" break case { it > 54 && it <= 58 }: MinPM25 = 54 MaxPM25 = 58 AQI = 7 AQIString = "High" AQIColor = "Red" break case { it > 59 && it <= 64 }: MinPM25 = 59 MaxPM25 = 64 AQI = 8 AQIString = "High" AQIColor = "Red" break case { it > 65 && it <= 70 }: MinPM25 = 65 MaxPM25 = 70 AQI = 9 AQIString = "High" AQIColor = "Red" break case { it > 71 }: MinPM25 = 71 MaxPM25 = 300 AQI = 10 AQIString = "Very High" AQIColor = "Purple" break } Handled = true break // Europe (CAQI) case "Europe (CAQI)": switch( Value ){ case { it <= 15 }: MinPM25 = 0 MaxPM25 = 15 MinAQI = 0 MaxAQI = 25 AQIString = "Very low" AQIColor = "Green" break case { it > 15 && it <= 30 }: MinPM25 = 15 MaxPM25 = 30 MinAQI = 25 MaxAQI = 50 AQIString = "Low" AQIColor = "Light Green" break case { it > 30 && it <= 55 }: MinPM25 = 30 MaxPM25 = 55 MinAQI = 50 MaxAQI = 75 AQIString = "Medium" AQIColor = "Gold" break case { it > 55 && it <= 110 }: MinPM25 = 55 MaxPM25 = 110 MinAQI = 75 MaxAQI = 100 AQIString = "High" AQIColor = "Orange" break case { it > 110 }: MinPM25 = 110 MaxPM25 = 250 MinAQI = 100 MaxAQI = 300 AQIString = "Very high" AQIColor = "Red" break } //AQI = Handled = true break // Russia case "Russia": break // United States case "United States": switch( Value ){ case { it <= 12.0 }: MinPM25 = 0 MaxPM25 = 12.0 MinAQI = 0 MaxAQI = 50 AQIString = "Good" AQIColor = "Green" break case { it >= 12.1 && it <= 35.4 }: MinPM25 = 12.1 MaxPM25 = 35.4 MinAQI = 51 MaxAQI = 100 AQIString = "Moderate" AQIColor = "Yellow" break case { it >= 35.5 && it <= 55.4 }: MinPM25 = 35.5 MaxPM25 = 55.4 MinAQI = 101 MaxAQI = 150 AQIString = "Unhealthy for Sensitive Groups" AQIColor = "Orange" break case { it >= 55.5 && it <= 150.4 }: MinPM25 = 55.5 MaxPM25 = 150.4 MinAQI = 151 MaxAQI = 200 AQIString = "Unhealthy" AQIColor = "Red" break case { it >= 150.5 && it <= 250.4 }: MinPM25 = 150.5 MaxPM25 = 250.4 MinAQI = 201 MaxAQI = 300 AQIString = "Very Unhealthy" AQIColor = "Purple" break case { it >= 250.5 && it <= 350.4 }: MinPM25 = 250.5 MaxPM25 = 350.4 MinAQI = 301 MaxAQI = 400 AQIString = "Hazardous" AQIColor = "Maroon" break case { it >= 350.5 }: MinPM25 = 350.5 MaxPM25 = 500.4 MinAQI = 401 MaxAQI = 500 AQIString = "Hazardous" AQIColor = "Maroon" break } AQI = Math.round( ( ( ( ( MaxAQI - MinAQI ) / ( MaxPM25 - MinPM25 ) ) * ( Value - MinPM25 ) ) + MinAQI ) ) Handled = true break case "Australia": switch( Value ){ case { it <= 25 }: MinPM25 = 0 MaxPM25 = 25 MinAQI = 0 MaxAQI = 33 AQIString = "Very Good" AQIColor = "Blue" break case { it >= 25 && it <= 50 }: MinPM25 = 25 MaxPM25 = 50 MinAQI = 34 MaxAQI = 66 AQIString = "Good" AQIColor = "Green" break case { it >= 50 && it <= 100 }: MinPM25 = 51 MaxPM25 = 100 MinAQI = 67 MaxAQI = 99 AQIString = "Fair" AQIColor = "Yellow" break case { it >= 100 && it <= 150 }: MinPM25 = 101 MaxPM25 = 150 MinAQI = 100 MaxAQI = 149 AQIString = "Poor" AQIColor = "Orange" break case { it >= 150 && it <= 250 }: MinPM25 = 150 MaxPM25 = 250 MinAQI = 150 MaxAQI = 200 AQIString = "Very Poor" AQIColor = "Maroon" break case { it > 250 }: MinPM25 = 250 MaxPM25 = 350 MinAQI = 200 MaxAQI = 300 AQIString = "Hazardous" AQIColor = "Red" break } // AQI = Handled = true break case "Undetermined": Logging( "Undetermined region, cannot calculate AQI at this time.", 3 ) break } if( Handled ){ def TempNaming if( Sensor == "Air Quality Sensor" ){ TempNaming = "Indoor Air Quality Index" } else { TempNaming = "Air Quality Index" } if( Average == true ){ TempNaming += " 24hr Average" } if( Sensor != null ){ if( SensorNumber != null ){ ProcessEvent( "${ TempNaming } ${ SensorNumber }", AQI ) ProcessEvent( "${ TempNaming } ${ SensorNumber } String", AQIString ) ProcessEvent( "${ TempNaming } ${ SensorNumber } Color", AQIColor ) PostEventToChild( "${ Sensor }${ SensorNumber }", "${ TempNaming }", AQI ) PostEventToChild( "${ Sensor }${ SensorNumber }", "${ TempNaming } String", AQIString ) PostEventToChild( "${ Sensor }${ SensorNumber }", "${ TempNaming } Color", AQIColor ) } else { ProcessEvent( "${ TempNaming }", AQI ) ProcessEvent( "${ TempNaming } String", AQIString ) ProcessEvent( "${ TempNaming } Color", AQIColor ) PostEventToChild( "${ Sensor }", "${ TempNaming }", AQI ) PostEventToChild( "${ Sensor }", "${ TempNaming } String", AQIString ) PostEventToChild( "${ Sensor }", "${ TempNaming } Color", AQIColor ) } } else { ProcessEvent( "${ TempNaming }", AQI ) ProcessEvent( "${ TempNaming } String", AQIString ) ProcessEvent( "${ TempNaming } Color", AQIColor ) } } } } // Process data to check against current state value and then send an event if it has changed def ProcessEvent( Variable, Value, Unit = null, ForceEvent = false ){ if( ( state."${ Variable }" != Value ) || ( ForceEvent == true ) ){ state."${ Variable }" = Value if( Unit != null ){ Logging( "Event: ${ Variable } = ${ Value }${ Unit }", 4 ) sendEvent( name: "${ Variable }", value: Value, unit: Unit, isStateChange: true ) } else { Logging( "Event: ${ Variable } = ${ Value }", 4 ) sendEvent( name: "${ Variable }", value: Value, isStateChange: true ) } //UpdateTile( "${ Value }" ) } } // Process data to check against current state value and then send an event if it has changed def ProcessState( Variable, Value ){ if( state."${ Variable }" != Value ){ Logging( "State: ${ Variable } = ${ Value }", 4 ) state."${ Variable }" = Value //UpdateTile( "${ Value }" ) } } // Post data to child device def PostEventToChild( Child, Variable, Value, Unit = null, ForceEvent = false ){ if( ChildrenEnabled ){ if( Child != null ){ if( getChildDevice( "${ Child }" ) == null ){ addSensor( "${ Child }" ) } if( getChildDevice( "${ Child }" ) != null ){ if( Unit != null ){ if( ForceEvent ){ getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value, "${ Unit }", ForceEvent ) Logging( "Child Event: ${ Variable } = ${ Value }${ Unit }", 4 ) } else { getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value, "${ Unit }" ) Logging( "Child Event: ${ Variable } = ${ Value }", 4 ) } } else { if( ForceEvent ){ getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value, null, ForceEvent ) Logging( "Child Event: ${ Variable } = ${ Value }${ Unit }", 4 ) } else { getChildDevice( "${ Child }" ).ProcessEvent( "${ Variable }", Value ) Logging( "Child Event: ${ Variable } = ${ Value }", 4 ) } } } else { if( Unit != null ){ Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }${ Unit }", 5 ) } else { Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }", 5 ) } } } else { Logging( "Failure to add child because child name was null", 5 ) } } } // Post data to child device def PostStateToChild( Child, Variable, Value ){ if( ChildrenEnabled ){ if( Child != null ){ if( getChildDevice( "${ Child }" ) == null ){ addSensor( "${ Child }" ) } if( getChildDevice( "${ Child }" ) != null ){ Logging( "${ Child } State: ${ Variable } = ${ Value }", 4 ) getChildDevice( "${ Child }" ).ProcessState( "${ Variable }", Value ) } else { Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }", 5 ) } } else { Logging( "Failure to add child because child name was null", 5 ) } } } // Adds a WeatherSensorChild child device // Based on @mircolino's method for child sensors def addSensor( String DNI ){ try{ Logging( "addSensor(${ DNI })", 3 ) addChildDevice( "WeatherSensorChild", DNI, [ name: "${ DNI }" ] ) } catch( Exception e ){ def Temp = e as String if( Temp.contains( "not found" ) ){ Logging( "WeatherSensorChild driver is not loaded, this is required for child devices.\n Disabling children for rest of refresh.", 5 ) ChildrenEnabled = false } else { Logging( "Exception in addSensor: ${ Temp }", 5 ) } } } // Handles whether logging is enabled and thus what to put there. def Logging( LogMessage, LogLevel ){ // Add all messages as info logging if( ( LogLevel == 2 ) && ( LogType != "None" ) ){ log.info( "${ device.displayName } - ${ LogMessage }" ) } else if( ( LogLevel == 3 ) && ( ( LogType == "Debug" ) || ( LogType == "Trace" ) ) ){ log.debug( "${ device.displayName } - ${ LogMessage }" ) } else if( ( LogLevel == 4 ) && ( LogType == "Trace" ) ){ log.trace( "${ device.displayName } - ${ LogMessage }" ) } else if( LogLevel == 5 ){ log.error( "${ device.displayName } - ${ LogMessage }" ) } } // Checks drdsnell.com for the latest version of the driver // Original inspiration from @cobra's version checking def CheckForUpdate(){ ProcessEvent( "DriverName", DriverName() ) ProcessEvent( "DriverVersion", DriverVersion() ) httpGet( uri: "https://www.drdsnell.com/projects/hubitat/drivers/versions.json", contentType: "application/json" ){ resp -> switch( resp.status ){ case 200: if( resp.data."${ DriverName() }" ){ CurrentVersion = DriverVersion().split( /\./ ) if( resp.data."${ DriverName() }".version == "REPLACED" ){ ProcessEvent( "DriverStatus", "Driver replaced, please use ${ resp.data."${ state.'DriverName' }".file }" ) } else if( resp.data."${ DriverName() }".version == "REMOVED" ){ ProcessEvent( "DriverStatus", "Driver removed and no longer supported." ) } else { SiteVersion = resp.data."${ DriverName() }".version.split( /\./ ) if( CurrentVersion == SiteVersion ){ Logging( "Driver version up to date", 3 ) ProcessEvent( "DriverStatus", "Up to date" ) } else if( ( CurrentVersion[ 0 ] as int ) > ( SiteVersion [ 0 ] as int ) ){ Logging( "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) ProcessEvent( "DriverStatus", "Major development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( CurrentVersion[ 1 ] as int ) > ( SiteVersion [ 1 ] as int ) ){ Logging( "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) ProcessEvent( "DriverStatus", "Minor development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( CurrentVersion[ 2 ] as int ) > ( SiteVersion [ 2 ] as int ) ){ Logging( "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version", 3 ) ProcessEvent( "DriverStatus", "Patch development ${ CurrentVersion[ 0 ] }.${ CurrentVersion[ 1 ] }.${ CurrentVersion[ 2 ] } version" ) } else if( ( SiteVersion[ 0 ] as int ) > ( CurrentVersion[ 0 ] as int ) ){ Logging( "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New major release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } else if( ( SiteVersion[ 1 ] as int ) > ( CurrentVersion[ 1 ] as int ) ){ Logging( "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New minor release ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } else if( ( SiteVersion[ 2 ] as int ) > ( CurrentVersion[ 2 ] as int ) ){ Logging( "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available", 2 ) ProcessEvent( "DriverStatus", "New patch ${ SiteVersion[ 0 ] }.${ SiteVersion[ 1 ] }.${ SiteVersion[ 2 ] } available" ) } } } else { Logging( "${ DriverName() } is not published on drdsnell.com", 2 ) ProcessEvent( "DriverStatus", "${ DriverName() } is not published on drdsnell.com" ) } break default: Logging( "Unable to check drdsnell.com for ${ DriverName() } driver updates.", 2 ) break } } }