/*
* 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 (Can vary depending on app you use to configure the gateway)
* Using the "WSView Plus" Android app:
* 1) Select the Ecowitt Gateway from the Live Devices
* 2) Select More
* 3) Select Weather Services
* 4) Select Next (or swipe through the list) until Customized is shown/selected
* 5) Set the Server IP/Hostname to match your Hubitat
* 6) Set the Port to be 39501
* 7) Leave the default Path as entered (/data/report)
* 8) Set the Upload Interval as desired (faster intervals may cause excessive events on your hub)
* 9) Save
*
* Using the "Ecowitt" Android app:
* 1) Select your Ecowitt Gateway from the list of devices
* 2) Select the menu "..." option
* 3) Select "Others" from the dropdown
* 4) Select the "DIY Upload Servers" button
* 5) Select the "Customized" icon
* 6) Select the "Enabled" or "Enable" button (Note: varies by whether typo is corrected or not, will only appear on first time)
* 7) On the customized page:
* a) Validate that the Protocol Type is set to Ecowitt
* b) Configure the Server IP / Host Name to match your Hubitat's IP / Hostname
* c) Configure the port for 39501
* d) Make sure the Path is data/report/ (Note: this is the default setting)
* e) Select an Upload Interval (Note: it is recommended to be 300 seconds or greater to minimize load on the Hubitat).
* 8) Select the Save button
*
* On the Hubitat side (after configuring your gateway using the app)
* 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 2025 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.14 - Removal of CO2 battery status if no other related data is provided
* 0.8.13 - Cleanup of Driver Status
* 0.8.12 - Added additional Gw2001-based value handling brought up by @MajorEvent, changed a lot of state references to repeating the returned value,
* added a LastUpdateString and LastUpdateEpoch for child devices when the device's battery value is reported and upon update for parent
* 0.8.11 - Added rrain_piezo for GW2001
* 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.14"
}
// 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 "LastUpdateString", "string"
attribute "LastUpdateEpoch", "number"
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 Status" != null ){
state.remove( "Driver Name" )
state.remove( "Driver Version" )
state.remove( "Driver Status" )
device.deleteCurrentState( "Driver Status" )
device.deleteCurrentState( "Driver Name" )
device.deleteCurrentState( "Driver Version" )
}
ProcessState( "DriverName", "${ DriverName() }" )
ProcessState( "DriverVersion", "${ DriverVersion() }" )
ProcessState( "DriverStatus", null )
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 ){
PostEventToChild( "Rain Sensor", "LastUpdateString", new Date(), null, true )
PostEventToChild( "Rain Sensor", "LastUpdateEpoch", new Date().getTime(), null, true )
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 )
}
ProcessEvent( "Last Updated", new Date(), null, true )
ProcessEvent( "LastUpdateString", new Date(), null, true )
ProcessEvent( "LastUpdateEpoch", new Date().getTime(), null, true )
}
// 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, Description = null ){
if( ForceEvent ){
sendEvent( name: Variable, value: Value, unit: Unit, isStateChange: true, descriptionText: Description )
} else {
sendEvent( name: Variable, value: Value, unit: Unit, descriptionText: Description )
}
Logging( "Event: ${ Variable } = ${ Value } Unit = ${ Unit } Forced = ${ ForceEvent }", 4 )
ProcessState( Variable, Value )
//UpdateTile( "${ Value }" )
}
// Set a state variable to a value
def ProcessState( 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 ){
addChild( "${ Child }" )
}
if( getChildDevice( Child ) != null ){
if( ForceEvent ){
getChildDevice( Child ).sendEvent( name: Variable, value: Value, unit: Unit, isStateChange: true )
getChildDevice( Child ).ProcessState( Variable, Value )
} else {
getChildDevice( Child ).sendEvent( name: Variable, value: Value, unit: Unit )
getChildDevice( Child ).ProcessState( Variable, Value )
}
} else {
Logging( "Failure to add ${ Child } and post ${ Variable }=${ Value }${ Unit }", 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 ){
addChild( "${ 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 addChild( String DNI ){
try{
Logging( "addChild(${ 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 addChild: ${ 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
}
}
}