/** * Driver: Ecowitt RF Sensor * Author: Mirco Caramori * Repository: https://github.com/mircolino/ecowitt * Import URL: https://raw.githubusercontent.com/mircolino/ecowitt/master/ecowitt_sensor.groovy * * 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. * * Change Log: shared with ecowitt_gateway.groovy * lgk add debugging with auto turn off so we can see if it is getting temp because it wont show an update event in hubitat logs if the same as last temp. * lgk add lastTemperatuer and lastHumidity attributes so we can easily write rules to trigger if the temp or humidity is going up or down. * I use this to alert when the hot tub will be at max in xx amount of time etc. * * lgk add code for capablity Air Quality using aqi * */ metadata { definition(name: "Ecowitt RF Sensor", namespace: "mircolino", author: "Mirco Caramori", importUrl: "https://raw.githubusercontent.com/mircolino/ecowitt/master/ecowitt_sensor.groovy") { capability "Sensor"; capability "Battery"; capability "Temperature Measurement"; capability "Relative Humidity Measurement"; capability "Pressure Measurement"; capability "Ultraviolet Index"; capability "Illuminance Measurement"; capability "Water Sensor"; capability "CarbonDioxide Measurement"; capability "Air Quality"; // attribute "battery", "number"; // 0-100% attribute "batteryIcon", "number"; // 0, 20, 40, 60, 80, 100 attribute "batteryOrg", "number"; // original/un-translated battery value returned by the sensor attribute "batteryTemp", "number"; // attribute "batteryTempIcon", "number"; // Only created/used when a WH32 is bundled in a PWS attribute "batteryTempOrg", "number"; // attribute "batteryRain", "number"; // attribute "batteryRainIcon", "number"; // Only created/used when a WH40 is bundled in a PWS attribute "batteryRainOrg", "number"; // attribute "batteryWind", "number"; // attribute "batteryWindIcon", "number"; // Only created/used when a WH68/WH80 is bundled in a PWS attribute "batteryWindOrg", "number"; // // attribute "temperature", "number"; // °F // attribute "humidity", "number"; // 0-100% attribute "humidityAbs", "number"; // oz/yd³ or g/m³ attribute "dewPoint", "number"; // °F - calculated using outdoor "temperature" & "humidity" attribute "heatIndex", "number"; // °F - calculated using outdoor "temperature" & "humidity" attribute "heatDanger", "string"; // Heat index danger level attribute "heatColor", "string"; // Heat index HTML color attribute "simmerIndex", "number"; // °F - calculated using outdoor "temperature" & "humidity" attribute "simmerDanger", "string"; // Summer simmmer index danger level attribute "simmerColor", "string"; // Summer simmer index HTML color // attribute "pressure", "number"; // inHg - relative pressure corrected to sea-level attribute "pressureAbs", "number"; // inHg - absolute pressure attribute "rainRate", "number"; // in/h - rainfall rate attribute "rainEvent", "number"; // in - rainfall in the current event attribute "rainHourly", "number"; // in - rainfall in the current hour attribute "rainDaily", "number"; // in - rainfall in the current day attribute "rainWeekly", "number"; // in - rainfall in the current week attribute "rainMonthly", "number"; // in - rainfall in the current month attribute "rainYearly", "number"; // in - rainfall in the current year attribute "rainTotal", "number"; // in - rainfall total since sensor installation attribute "pm25", "number"; // µg/m³ - PM2.5 particle reading - current attribute "pm25_avg_24h", "number"; // µg/m³ - PM2.5 particle reading - average over the last 24 hours attribute "pm10", "number"; // µg/m³ - PM10 particle reading - current attribute "pm10_avg_24h", "number"; // µg/m³ - PM10 particle reading - average over the last 24 hours // attribute "co2", "number"; // ppm - CO2 concetration - current attribute "carbonDioxide_avg_24h", "number"; // ppm - CO2 concetration - average over the last 24 hours attribute "aqi", "number"; // AQI (0-500) attribute "aqiDanger", "string"; // AQI danger level attribute "aqiColor", "string"; // AQI HTML color attribute "aqi_avg_24h", "number"; // AQI (0-500) - average over the last 24 hours attribute "aqiDanger_avg_24h", "string"; // AQI danger level - average over the last 24 hours attribute "aqiColor_avg_24h", "string"; // AQI HTML color - average over the last 24 hours // attribute "water", "enum", ["dry", "wet"]; // "dry" or "wet" attribute "waterMsg", "string"; // dry) "Dry", wet) "Leak detected!" attribute "waterColor", "string"; // dry) "ffffff", wet) "ff0000" to colorize the icon attribute "lightningTime", "string"; // Strike time - local time attribute "lightningDistance", "number"; // Strike distance - km attribute "lightningEnergy", "number"; // Strike energy - MJ/m attribute "lightningCount", "number"; // Strike total count // attribute "ultravioletIndex", "number"; // UV index (0-11+) attribute "ultravioletDanger", "string"; // UV danger (0-2.9) Low, (3-5.9) Medium, (6-7.9) High, (8-10.9) Very High, (11+) Extreme attribute "ultravioletColor", "string"; // UV HTML color // attribute "illuminance", "number"; // lux attribute "solarRadiation", "number"; // W/m² attribute "windDirection", "number"; // 0-359° attribute "windCompass", "string"; // NNE attribute "windDirection_avg_10m", "number"; // 0-359° - average over the last 10 minutes attribute "windCompass_avg_10m", "string"; // NNE - average over the last 10 minutes attribute "windSpeed", "number"; // mph attribute "windSpeed_avg_10m", "number"; // mph - average over the last 10 minutes attribute "windGust", "number"; // mph attribute "windGustMaxDaily", "number"; // mph - max in the current day attribute "windChill", "number"; // °F - calculated using outdoor "temperature" & "windSpeed" attribute "windDanger", "string"; // Windchill danger level attribute "windColor", "string"; // Windchill HTML color attribute "html", "string"; // attribute "html1", "string"; // attribute "html2", "string"; // e.g. "
Temperature: ${temperature}°F
Humidity: ${humidity}%
" attribute "html3", "string"; // attribute "html4", "string"; // attribute "status", "string"; // Display current driver status attribute "orphaned", "enum", ["false", "true"]; // Whether or not the unbundled sensor is still receiving data from the gateway attribute "orphanedTemp", "enum", ["false", "true"]; // Whether or not the bundled WH32 is still receiving data from the gateway attribute "orphanedRain", "enum", ["false", "true"]; // Whether or not the bundled WH40 is still receiving data from the gateway attribute "orphanedWind", "enum", ["false", "true"]; // Whether or not the bundled WH68/WH80 sensor is still receiving data from the gateway attribute "lastUpdate", "string" attribute "bundled", "string" attribute "rainLastUpdate", "string" attribute "windLastUpdate", "string" attribute "temperatureLastUpdate", "string" attribute "humidityLastUpdate", "string" attribute "lightningLastUpdate", "string" attribute "lastTemperature", "number" attribute "lastHumidity", "number" attribute "temperatureChange", "number" attribute "humidityChange", "number" } preferences { input(name: "htmlTemplate", type: "string", title: "Tile HTML Template(s)", description: "See documentation for input formats", defaultValue: ""); input("debug", "bool", title: "Enable logging?", required: true, defaultValue: false) if (localAltitude != null) { input(name: "localAltitude", type: "string", title: "Altitude to Correct Sea Level Pressure", description: "Examples: \"378 ft\" or \"115 m\"", defaultValue: "", required: true); } if (voltageMin != null) { input(name: "voltageMin", type: "string", title: "Empty Battery Voltage", description: "Sensor value when battery is empty", defaultValue: "", required: true); input(name: "voltageMax", type: "string", title: "Full Battery Voltage", description: "Sensor value when battery is full", defaultValue: "", required: true); } if (calcDewPoint != null) { input(name: "calcDewPoint", type: "bool", title: "Calculate Dew Point & Absolute Humidity", description: "Temperature below which water vapor will condense & amount of water contained in a parcel of air", defaultValue: false); } if (calcHeatIndex != null) { input(name: "calcHeatIndex", type: "bool", title: "Calculate Heat Index", description: "Perceived discomfort as a result of the combined effects of the air temperature and humidity", defaultValue: false); } if (calcSimmerIndex != null) { input(name: "calcSimmerIndex", type: "bool", title: "Calculate Summer Simmer Index", description: "Similar to the Heat Index but using a newer and more accurate formula", defaultValue: false); } if (calcWindChill != null) { input(name: "calcWindChill", type: "bool", title: "Calculate Wind-chill Factor", description: "Lowering of body temperature due to the passing-flow of lower-temperature air", defaultValue: false); } } } /* * State variables used by the driver: * * sensor \ * sensorTemp | null) not present, 0) waiting to receive data, 1) processing data * sensorRain | * sensorWind / * */ /* * Data variables used by the driver: * * "isBundled" // "true" if we are a bundled PWS (set by the parent at creation time) * "htmlTemplate" // User template 0 * "htmlTemplate1" // User template 1 * "htmlTemplate2" // User template 2 * "htmlTemplate3" // User template 3 * "htmlTemplate4" // User template 4 */ // Logging -------------------------------------------------------------------------------------------------------------------- private void logError(String str) { log.error(str); } private void logWarning(String str) { if (getParent().logGetLevel() > 0) log.warn(str); } private void logInfo(String str) { if (getParent().logGetLevel() > 1) log.info(str); } private void logDebug(String str) { if (getParent().logGetLevel() > 2) log.debug(str); } private void logTrace(String str) { if (getParent().logGetLevel() > 3) log.trace(str); } // Ztatus --------------------------------------------------------------------------------------------------------------------- private Boolean ztatus(String str, String color = null) { if (color) str = "${str}"; return (attributeUpdateString(str, "status")); } // ------------------------------------------------------------ private Boolean ztatusIsError() { String str = device.currentValue("status") as String; if (str && str.contains("")) return (true); return (false); } // Conversions ---------------------------------------------------------------------------------------------------------------- private Boolean unitSystemIsMetric() { // // Return true if the selected unit system is metric // return (getParent().unitSystemIsMetric()); } // ------------------------------------------------------------ private String timeEpochToLocal(String time) { // // Convert Unix Epoch time (seconds) to local time with locale format // try { Long epoch = time.toLong() * 1000L; Date date = new Date(epoch); java.text.SimpleDateFormat format = new java.text.SimpleDateFormat(); time = format.format(date); } catch (Exception e) { logError("Exception in timeEpochToLocal(): ${e}"); } return (time); } // ------------------------------------------------------------ private BigDecimal convertRange(BigDecimal val, BigDecimal inMin, BigDecimal inMax, BigDecimal outMin, BigDecimal outMax, Boolean returnInt = true) { // Let make sure ranges are correct assert (inMin <= inMax); assert (outMin <= outMax); // Restrain input value if (val < inMin) val = inMin; else if (val > inMax) val = inMax; val = ((val - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin; if (returnInt) { // If integer is required we use the Float round because the BigDecimal one is not supported/not working on Hubitat val = val.toFloat().round().toBigDecimal(); } return (val); } // ------------------------------------------------------------ private BigDecimal convert_F_to_C(BigDecimal val) { return ((val - 32) / 1.8); } // ------------------------------------------------------------ private BigDecimal convert_C_to_F(BigDecimal val) { return ((val * 1.8) + 32); } // ------------------------------------------------------------ private BigDecimal convert_inHg_to_hPa(BigDecimal val) { return (val * 33.863886666667); } // ------------------------------------------------------------ private BigDecimal convert_hPa_to_inHg(BigDecimal val) { return (val / 33.863886666667); } // ------------------------------------------------------------ private BigDecimal convert_in_to_mm(BigDecimal val) { return (val * 25.4); } // ------------------------------------------------------------ private BigDecimal convert_mm_to_in(BigDecimal val) { return (val / 25.4); } // ------------------------------------------------------------ private BigDecimal convert_ft_to_m(BigDecimal val) { return (val / 3.28084); } // ------------------------------------------------------------ private BigDecimal convert_m_to_ft(BigDecimal val) { return (val * 3.28084); } // ------------------------------------------------------------ private BigDecimal convert_mi_to_km(BigDecimal val) { return (val * 1.609344); } // ------------------------------------------------------------ private BigDecimal convert_km_to_mi(BigDecimal val) { return (val / 1.609344); } // ------------------------------------------------------------ private BigDecimal convert_Wm2_to_lux(BigDecimal val) { return (val / 0.0079); } // ------------------------------------------------------------ private BigDecimal convert_lux_to_Wm2(BigDecimal val) { return (val * 0.0079); } // ------------------------------------------------------------ private BigDecimal convert_gm3_to_ozyd3(BigDecimal val) { return (val / 37.079776); } // ------------------------------------------------------------ private BigDecimal convert_ozyd3_to_gm3(BigDecimal val) { return (val * 37.079776); } // Attribute handling ---------------------------------------------------------------------------------------------------------- private Boolean attributeUpdateString(String val, String attribute) { // // Only update "attribute" if different // Return true if "attribute" has actually been updated/created // if ((device.currentValue(attribute) as String) != val) { sendEvent(name: attribute, value: val); return (true); } return (false); } // ------------------------------------------------------------ private Boolean attributeUpdateNumber(BigDecimal val, String attribute, String measure = null, Integer decimals = -1) { // // Only update "attribute" if different // Return true if "attribute" has actually been updated/created // // If rounding is required we use the Float one because the BigDecimal is not supported/not working on Hubitat if (debug) log.debug "in attr update number val [ $val attribute = $attribute" if (decimals >= 0) val = val.toFloat().round(decimals).toBigDecimal(); BigDecimal integer = val.toBigInteger(); // We don't strip zeros on an integer otherwise it gets converted to scientific exponential notation val = (val == integer)? integer: val.stripTrailingZeros(); // Coerce Object -> BigDecimal if ((device.currentValue(attribute) as BigDecimal) != val) { if (measure) sendEvent(name: attribute, value: val, unit: measure); else sendEvent(name: attribute, value: val); return (true); } return (false); } // ------------------------------------------------------------ private List attributeEnumerate(Boolean existing = true) { // // Return a list of all available attributes // If "existing" == true return only those that have been already created (non-null ones) // Returned list can be empty but never return null // List list = []; List attrib = device.getSupportedAttributes(); if (attrib) { attrib.each { if (existing == false || device.currentValue(it.name) != null) list.add(it.name); } } return (list); } // ------------------------------------------------------------ private Boolean attributeUpdateBattery(String val, String attribBattery, String attribBatteryIcon, String attribBatteryOrg, Integer type) { // // Convert all different batteries returned values to a 0-100% range // Type: 1) voltage: range from 1.30V (empty) to 1.65V (full) // 2) pentastep: range from 0 (empty) to 5 (full) // 0) binary: 0 (full) or 1 (empty) // BigDecimal original = val.toBigDecimal(); BigDecimal percent; BigDecimal icon; String unitOrg; // log.debug "in attribute update battery val = $val attrib = $attribBattery type = $type" switch (type) { case 1: // Change range from voltage to (0% - 100%) BigDecimal vMin, vMax; if (!(settings.voltageMin) || !(settings.voltageMax)) { // First time: initialize and show the preference vMin = 1.3; vMax = 1.65; device.updateSetting("voltageMin", [value: vMin, type: "string"]); device.updateSetting("voltageMax", [value: vMax, type: "string"]); } else { vMin = (settings.voltageMin).toBigDecimal(); vMax = (settings.voltageMax).toBigDecimal(); } percent = convertRange(original, vMin, vMax, 0, 100); unitOrg = "V"; break; case 2: // Change range from (0 - 5) to (0% - 100%) percent = convertRange(original, 0, 5, 0, 100); unitOrg = "level"; break; default: // Change range from (0 or 1) to (100% or 0%) percent = (original == 0)? 100: 0; unitOrg = "!bool"; } if (percent < 10) icon = 0; else if (percent < 30) icon = 20; else if (percent < 50) icon = 40; else if (percent < 70) icon = 60; else if (percent < 90) icon = 80; else icon = 100; Boolean updated = attributeUpdateNumber(percent, attribBattery, "%", 0); if (attributeUpdateNumber(icon, attribBatteryIcon, "%")) updated = true; if (attributeUpdateNumber(original, attribBatteryOrg, unitOrg)) updated = true; return (updated); } // ----------------------------- private Boolean attributeUpdateLowestBattery() { BigDecimal percent = 100; String org = "0"; Integer type = 0; BigDecimal temp = device.currentValue("batteryTemp") as BigDecimal; BigDecimal rain = device.currentValue("batteryRain") as BigDecimal; BigDecimal wind = device.currentValue("batteryWind") as BigDecimal; if (temp != null) { percent = temp; org = device.currentValue("batteryTempOrg") as String; type = 0; } if (rain != null && rain < percent) { percent = rain; org = device.currentValue("batteryRainOrg") as String; type = 1; } if (wind != null && wind < percent) { percent = wind; org = device.currentValue("batteryWindOrg") as String; type = 1; } return (attributeUpdateBattery(org, "battery", "batteryIcon", "batteryOrg", type)); } // ------------------------------------------------------------ private Boolean attributeUpdateTemperature(String val, String attribTemperature) { BigDecimal degrees = val.toBigDecimal(); String measure = "°F"; // Convert to metric if requested if (unitSystemIsMetric()) { degrees = convert_F_to_C(degrees); measure = "°C"; } Boolean hasChanged = (attributeUpdateNumber(degrees, attribTemperature, measure, 1)) // only do this if actually temp not other attributes that come through here like dew pt if (attribTemperature == "temperature") { def lastTemp = (device.currentValue(attribTemperature) as BigDecimal) BigDecimal change = 0.00 if (lastTemp != null) change = (degrees - lastTemp as BigDecimal) else lastTemp = 0.00 attributeUpdateNumber(lastTemp,"lastTemperature",measure,1) if (debug) log.debug "In update temp val = $val , measure = $measure, attribute = $attribTemperature, lastTemp = $lastTemp, change = $change, hasChanged = $hasChanged" // only log difference if we have a changed value. if (hasChanged == true) { sendEvent(name: "temperatureChange", value: change) } else { sendEvent(name: "temperatureChange", value: 0.00) } } return hasChanged } // ------------------------------------------------------------ private Boolean attributeUpdateHumidity(String val, String attribHumidity) { BigDecimal percent = val.toBigDecimal(); def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone) sendEvent(name: "humidityLastUpdate", value: now) def lastHumid = (device.currentValue(attribHumidity) as BigDecimal) BigDecimal change = 0.00 if (lastHumid != null) change = (percent - lastHumid as BigDecimal) else lastHumid = 0.00 attributeUpdateNumber(lastHumid,"lastHumidity","%",0) Boolean hasChanged = (attributeUpdateNumber(percent, attribHumidity, "%", 0)) // only log difference if we have a changed value. if (hasChanged == true) sendEvent(name: "humidityChange", value: change) else sendEvent(name: "humidityChange", value: 0.00) return hasChanged } // ------------------------------------------------------------ private Boolean attributeUpdatePressure(String val, String attribPressure, String attribPressureAbs) { // Get unit system Boolean metric = unitSystemIsMetric(); // Get pressure in hectopascal BigDecimal absolute = convert_inHg_to_hPa(val.toBigDecimal()); // Get altitude in meters val = settings.localAltitude; if (!val) { // First time: initialize and show the preference val = metric? "0 m": "0 ft"; device.updateSetting("localAltitude", [value: val, type: "string"]); } BigDecimal altitude; try { String[] field = val.split(); altitude = field[0].toBigDecimal(); if (field.size() == 1) { // No unit found: let's use the parent setting if (!metric) altitude = convert_ft_to_m(altitude); } else { // Found a unit: convert accordingly if (field[1][0] == "f" || field[1][0] == "F") altitude = convert_ft_to_m(altitude); } } catch(Exception ignored) { altitude = 0; } // Get temperature in celsious BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature == null) temperature = 18; else if (!metric) temperature = convert_F_to_C(temperature); // Correct pressure to sea level using this conversion formula: https://keisan.casio.com/exec/system/1224575267 BigDecimal relative = absolute * Math.pow(1 - ((altitude * 0.0065) / (temperature + (altitude * 0.0065) + 273.15)), -5.257); // Convert to imperial if requested if (metric) val = "hPa"; else { absolute = convert_hPa_to_inHg(absolute); relative = convert_hPa_to_inHg(relative); val = "inHg"; } Boolean updated = attributeUpdateNumber(relative, attribPressure, val, 2); if (attributeUpdateNumber(absolute, attribPressureAbs, val, 2)) updated = true; return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateRain(String val, String attribRain, Boolean hour = false) { BigDecimal amount = val.toBigDecimal(); String measure = hour? "in/h": "in"; // Convert to metric if requested if (unitSystemIsMetric()) { amount = convert_in_to_mm(amount); measure = hour? "mm/h": "mm"; } def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone) sendEvent(name: "rainLastUpdate", value: now) attributeUpdateString("false", "orphanedRain"); return (attributeUpdateNumber(amount, attribRain, measure, 2)); } // ------------------------------------------------------------ private Boolean attributeUpdatePM(String val, String attribPm) { BigDecimal pm = val.toBigDecimal(); return (attributeUpdateNumber(pm, attribPm, "µg/m³")); } // ------------------------------------------------------------ private Boolean attributeUpdateAQI(String val, Boolean pm25, String attribAqi, String attribAqiDanger, String attribAqiColor) { // // Conversions based on https://en.wikipedia.org/wiki/Air_quality_index // BigDecimal pm = val.toBigDecimal(); BigDecimal aqi; if (pm25) { // PM2.5 if (pm < 12.1) aqi = convertRange(pm, 0.0, 12.0, 0, 50); else if (pm < 35.5) aqi = convertRange(pm, 12.1, 35.4, 51, 100); else if (pm < 55.5) aqi = convertRange(pm, 35.5, 55.4, 101, 150); else if (pm < 150.5) aqi = convertRange(pm, 55.5, 150.4, 151, 200); else if (pm < 250.5) aqi = convertRange(pm, 150.5, 250.4, 201, 300); else if (pm < 350.5) aqi = convertRange(pm, 250.5, 350.4, 301, 400); else aqi = convertRange(pm, 350.5, 500.4, 401, 500); } else { // PM10 if (pm < 55) aqi = convertRange(pm, 0, 54, 0, 50); else if (pm < 155) aqi = convertRange(pm, 55, 154, 51, 100); else if (pm < 255) aqi = convertRange(pm, 155, 254, 101, 150); else if (pm < 355) aqi = convertRange(pm, 255, 354, 151, 200); else if (pm < 425) aqi = convertRange(pm, 355, 424, 201, 300); else if (pm < 505) aqi = convertRange(pm, 425, 504, 301, 400); else aqi = convertRange(pm, 505, 604, 401, 500); // Choose the highest AQI between PM2.5 and PM10 BigDecimal aqi25 = (device.currentValue(attribAqi) as BigDecimal); if (aqi < aqi25) aqi = aqi25; } String danger; String color; if (aqi < 51) { danger = "Good"; color = "3ea72d"; } else if (aqi < 101) { danger = "Moderate"; color = "fff300"; } else if (aqi < 151) { danger = "Unhealthy for Sensitive Groups"; color = "f18b00"; } else if (aqi < 201) { danger = "Unhealthy"; color = "e53210"; } else if (aqi < 301) { danger = "Very Unhealthy"; color = "b567a4"; } else if (aqi < 401) { danger = "Hazardous"; color = "7e0023"; } else { danger = "Hazardous"; color = "7e0023"; } // lgk set airQualityIndex only if actual aqi not avg if (attribAqi == "aqi") attributeUpdateNumber(aqi, "airQualityIndex", "AQI"); Boolean updated = attributeUpdateNumber(aqi, attribAqi, "AQI"); if (attributeUpdateString(danger, attribAqiDanger)) updated = true; if (attributeUpdateString(color, attribAqiColor)) updated = true; return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateCarbonDioxide(String val, String attribCo2) { BigDecimal co2 = val.toBigDecimal(); return (attributeUpdateNumber(co2, attribCo2, "ppm")); } // ------------------------------------------------------------ private Boolean attributeUpdateLeak(String val, String attribWater, String attribWaterMsg, String attribWaterColor) { BigDecimal leak = (val.toBigDecimal())? 1: 0; String water, message, color; if (leak) { water = "wet"; message = "Leak detected!"; color = "ff0000"; } else { water = "dry"; message = "Dry"; color = "ffffff"; } Boolean updated = attributeUpdateString(water, attribWater); if (attributeUpdateString(message, attribWaterMsg)) updated = true; if (attributeUpdateString(color, attribWaterColor)) updated = true; return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateLightningDistance(String val, String attrib) { if (!val) val = "0"; BigDecimal distance = val.toBigDecimal(); String measure = "km"; // Convert to imperial if requested if (unitSystemIsMetric() == false) { distance = convert_km_to_mi(distance); measure = "mi"; } return (attributeUpdateNumber(distance, attrib, measure, 1)); } // ------------------------------------------------------------ private Boolean attributeUpdateLightningCount(String val, String attrib) { if (!val) val = "0"; def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone) sendEvent(name: "lightningLastUpdate", value: now) return (attributeUpdateNumber(val.toBigDecimal(), attrib)); } // ------------------------------------------------------------ private Boolean attributeUpdateLightningTime(String val, String attrib) { val = (!val || val == "0")? "n/a": timeEpochToLocal(val); return (attributeUpdateString(val, attrib)); } // ------------------------------------------------------------ private Boolean attributeUpdateLightningEnergy(String val, String attrib) { if (!val) val = "0"; return (attributeUpdateNumber(val.toBigDecimal(), attrib, "MJ/m", 1)); } // ------------------------------------------------------------ private Boolean attributeUpdateUV(String val, String attribUvIndex, String attribUvDanger, String attribUvColor) { // // Conversions based on https://en.wikipedia.org/wiki/Ultraviolet_index // BigDecimal index = val.toBigDecimal(); String danger; String color; if (index < 3) { danger = "Low"; color = "3ea72d"; } else if (index < 6) { danger = "Medium"; color = "fff300"; } else if (index < 8) { danger = "High"; color = "f18b00"; } else if (index < 11) { danger = "Very High"; color = "e53210"; } else { danger = "Extreme"; color = "b567a4"; } Boolean updated = attributeUpdateNumber(index, attribUvIndex, "uvi"); if (attributeUpdateString(danger, attribUvDanger)) updated = true; if (attributeUpdateString(color, attribUvColor)) updated = true; return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateLight(String val, String attribSolarRadiation, String attribIlluminance) { BigDecimal light = val.toBigDecimal(); Boolean updated = attributeUpdateNumber(light, attribSolarRadiation, "W/m²"); if (attributeUpdateNumber(convert_Wm2_to_lux(light), attribIlluminance, "lux", 0)) updated = true; return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateWindSpeed(String val, String attribWindSpeed) { BigDecimal speed = val.toBigDecimal(); String measure = "mph"; // Convert to metric if requested if (unitSystemIsMetric()) { speed = convert_mi_to_km(speed); measure = "km/h"; } def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone) sendEvent(name: "windLastUpdate", value: now) attributeUpdateString("false", "orphanedWind"); return (attributeUpdateNumber(speed, attribWindSpeed, measure, 1)); } // ------------------------------------------------------------ private Boolean attributeUpdateWindDirection(String val, String attribWindDirection, String attribWindCompass) { BigDecimal direction = val.toBigDecimal(); // BigDecimal doesn't support modulo operation so we roll up our own direction = direction - (direction.divideToIntegralValue(360) * 360); String compass; if (direction >= 348.75 || direction < 11.25) compass = "N"; else if (direction < 33.75) compass = "NNE"; else if (direction < 56.25) compass = "NE"; else if (direction < 78.75) compass = "ENE"; else if (direction < 101.25) compass = "E"; else if (direction < 123.75) compass = "ESE"; else if (direction < 146.25) compass = "SE"; else if (direction < 168.75) compass = "SSE"; else if (direction < 191.25) compass = "S"; else if (direction < 213.75) compass = "SSW"; else if (direction < 236.25) compass = "SW"; else if (direction < 258.75) compass = "WSW"; else if (direction < 281.25) compass = "W"; else if (direction < 303.75) compass = "WNW"; else if (direction < 326.25) compass = "NW"; else compass = "NNW"; Boolean updated = attributeUpdateNumber(direction, attribWindDirection, "°"); if (attributeUpdateString(compass, attribWindCompass)) updated = true; return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateDewPoint(String val, String attribDewPoint, String attribHumidityAbs) { Boolean updated = false; BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature != null) { if (settings.calcDewPoint == null) { // First time: initialize and show the preference device.updateSetting("calcDewPoint", [value: false, type: "bool"]); } else if (settings.calcDewPoint) { if (!unitSystemIsMetric()) { // Convert temperature to C temperature = convert_F_to_C(temperature); } // Calculate dewPoint based on https://web.archive.org/web/20150209041650/http://www.gorhamschaffler.com:80/humidity_formulas.htm BigDecimal humidity = val.toBigDecimal(); double tC = temperature as double; // Calculate saturation vapor pressure in millibars BigDecimal e = (tC < 0) ? 6.1115 * Math.exp((23.036 - (tC / 333.7)) * (tC / (279.82 + tC))) : 6.1121 * Math.exp((18.678 - (tC / 234.4)) * (tC / (257.14 + tC))); // Calculate current vapor pressure in millibars e *= humidity / 100; BigDecimal degrees = (-430.22 + 237.7 * Math.log(e)) / (-Math.log(e) + 19.08); // Calculate humidityAbs based on https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/ BigDecimal volume = ((6.1121 * Math.exp((17.67 * tC) / (tC + 243.5)) * (humidity as double) * 2.1674)) / (tC + 273.15); if (!unitSystemIsMetric()) { degrees = convert_C_to_F(degrees); volume = convert_gm3_to_ozyd3(volume); } if (attributeUpdateTemperature(degrees.toString(), attribDewPoint)) updated = true; if (attributeUpdateNumber(volume, attribHumidityAbs, unitSystemIsMetric()? "g/m³": "oz/yd³", 2)) updated = true; } } return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateHeatIndex(String val, String attribHeatIndex, String attribHeatDanger, String attribHeatColor) { Boolean updated = false; BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature != null) { if (settings.calcHeatIndex == null) { // First time: initialize and show the preference device.updateSetting("calcHeatIndex", [value: false, type: "bool"]); } else if (settings.calcHeatIndex) { if (unitSystemIsMetric()) { // Convert temperature back to F temperature = convert_C_to_F(temperature); } // Calculate heatIndex based on https://en.wikipedia.org/wiki/Heat_index BigDecimal degrees; String danger; String color; if (temperature < 80) { degrees = temperature; danger = "Safe"; color = "ffffff"; } else { BigDecimal humidity = val.toBigDecimal(); degrees = -42.379 + ( 2.04901523 * temperature) + ( 10.14333127 * humidity) - ( 0.22475541 * (temperature * humidity)) - ( 0.00683783 * (temperature ** 2)) - ( 0.05481717 * (humidity ** 2)) + ( 0.00122874 * ((temperature ** 2) * humidity)) + ( 0.00085282 * (temperature * (humidity ** 2))) - ( 0.00000199 * ((temperature ** 2) * (humidity ** 2))); if (degrees < 80) { danger = "Safe"; color = "ffffff"; } else if (degrees < 91) { danger = "Caution"; color = "ffff66"; } else if (degrees < 104) { danger = "Extreme Caution"; color = "ffd700"; } else if (degrees < 126) { danger = "Danger"; color = "ff8c00"; } else { danger = "Extreme Danger"; color = "ff0000"; } } updated = attributeUpdateTemperature(degrees.toString(), attribHeatIndex); if (attributeUpdateString(danger, attribHeatDanger)) updated = true; if (attributeUpdateString(color, attribHeatColor)) updated = true; } } return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateSimmerIndex(String val, String attribSimmerIndex, String attribSimmerDanger, String attribSimmerColor) { Boolean updated = false; BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature != null) { if (settings.calcSimmerIndex == null) { // First time: initialize and show the preference device.updateSetting("calcSimmerIndex", [value: false, type: "bool"]); } else if (settings.calcSimmerIndex) { if (unitSystemIsMetric()) { // Convert temperature back to F temperature = convert_C_to_F(temperature); } // Calculate heatIndex based on https://www.vcalc.com/wiki/rklarsen/Summer+Simmer+Index BigDecimal degrees; String danger; String color; if (temperature < 70) { degrees = temperature; danger = "Cool"; color = "ffffff"; } else { BigDecimal humidity = val.toBigDecimal(); degrees = 1.98 * (temperature - (0.55 - (0.0055 * humidity)) * (temperature - 58.0)) - 56.83; if (degrees < 70) { danger = "Cool"; color = "ffffff"; } else if (degrees < 77) { danger = "Slightly Cool"; color = "0099ff"; } else if (degrees < 83) { danger = "Comfortable"; color = "2dca02"; } else if (degrees < 91) { danger = "Slightly Warm"; color = "9acd32"; } else if (degrees < 100) { danger = "Increased Discomfort"; color = "ffb233"; } else if (degrees < 112) { danger = "Caution Heat Exhaustion"; color = "ff6600"; } else if (degrees < 125) { danger = "Danger Heatstroke"; color = "ff3300"; } else if (degrees < 150) { danger = "Extreme Danger"; color = "ff0000"; } else { danger = "Circulatory Collapse Imminent"; color = "cc3300"; } } updated = attributeUpdateTemperature(degrees.toString(), attribSimmerIndex); if (attributeUpdateString(danger, attribSimmerDanger)) updated = true; if (attributeUpdateString(color, attribSimmerColor)) updated = true; } } return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateWindChill(String val, String attribWindChill, String attribWindDanger, String attribWindColor) { Boolean updated = false; BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature != null) { if (settings.calcWindChill == null) { // First time: initialize and show the preference device.updateSetting("calcWindChill", [value: false, type: "bool"]); } else if (settings.calcWindChill) { if (unitSystemIsMetric()) { // Convert temperature back to F temperature = convert_C_to_F(temperature); } // Calculate windChill based on https://en.wikipedia.org/wiki/Wind_chill BigDecimal degrees; String danger; String color; BigDecimal windSpeed = val.toBigDecimal(); if (temperature > 50 || windSpeed < 3) { degrees = temperature; danger = "Safe"; color = "ffffff"; } else { degrees = 35.74 + ( 0.6215 * temperature) - (35.75 * (windSpeed ** 0.16)) + ((0.4275 * temperature) * (windSpeed ** 0.16)); if (degrees < -69) { danger = "Frostbite certain"; color = "2d2c52"; } else if (degrees < -19) { danger = "Frostbite likely"; color = "1f479f"; } else if (degrees < 1) { danger = "Frostbite possible"; color = "0c6cb5"; } else if (degrees < 21) { danger = "Very Unpleasant"; color = "2f9fda"; } else if (degrees < 41) { danger = "Unpleasant"; color = "9dc8e6"; } else { danger = "Safe"; color = "ffffff"; } } updated = attributeUpdateTemperature(degrees.toString(), attribWindChill); if (attributeUpdateString(danger, attribWindDanger)) updated = true; if (attributeUpdateString(color, attribWindColor)) updated = true; } } return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateHtml(String templHtml, String attribHtml) { Boolean updated = false; String pattern = /\$\{([^}]+)\}/; String index; String val; for (Integer idx = 0; idx < 16; idx++) { index = idx? "${idx}": ""; val = device.getDataValue("${templHtml}${index}"); if (!val) break; val = val.replaceAll(~pattern) { java.util.ArrayList match -> (device.currentValue(match[1].trim()) as String); } if (attributeUpdateString(val, "${attribHtml}${index}")) updated = true; } return (updated); } // ------------------------------------------------------------ Boolean attributeUpdate(String key, String val) { // // Dispatch attributes changes to hub // Boolean updated = false; Boolean bundled = device.getDataValue("isBundled"); def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone) sendEvent(name: "lastUpdate", value: now) // log.debug "got key $key val = $val" switch (key) { case "wh26batt": if (bundled) { state.sensorTemp = 1; updated = attributeUpdateBattery(val, "batteryTemp", "batteryTempIcon", "batteryTempOrg", 0); // !boolean } else { state.sensor = 1; updated = attributeUpdateBattery(val, "battery", "batteryIcon", "batteryOrg", 0); } break; case "wh40batt": if (bundled) { state.sensorRain = 1; updated = attributeUpdateBattery(val, "batteryRain", "batteryRainIcon", "batteryRainOrg", 1); // voltage } else { state.sensor = 1; updated = attributeUpdateBattery(val, "battery", "batteryIcon", "batteryOrg", 1); } break; case "wh68batt": case "wh80batt": if (bundled) { state.sensorWind = 1; updated = attributeUpdateBattery(val, "batteryWind", "batteryWindIcon", "batteryWindOrg", 1); // voltage } else { state.sensor = 1; updated = attributeUpdateBattery(val, "battery", "batteryIcon", "batteryOrg", 1); } break; case ~/batt[1-8]/: case "wh25batt": case "wh65batt": case "ws90batt": case "wh90batt": state.sensor = 1; updated = attributeUpdateBattery(val, "battery", "batteryIcon", "batteryOrg", 0); // !boolean break; case ~/batt_wf[1-8]/: case ~/soilbatt[1-8]/: case ~/tf_batt[1-8]/: state.sensor = 1; updated = attributeUpdateBattery(val, "battery", "batteryIcon", "batteryOrg", 1); // voltage break; case ~/pm25batt[1-4]/: case ~/leakbatt[1-4]/: case "wh57batt": case "co2_batt": state.sensor = 1; updated = attributeUpdateBattery(val, "battery", "batteryIcon", "batteryOrg", 2); // 0 - 5 break; case "tempinf": case "tempf": case ~/tempf_wf[1-8]/: case ~/temp[1-8]f/: case ~/tf_ch[1-8]/: case "tf_co2": state.sensor = 1; if (debug) log.debug "Updating Temp: $val" updated = attributeUpdateTemperature(val, "temperature"); if (updated) sendEvent(name: "temperatureLastUpdate", value: now) break; case "humidity": case "humidityin": case ~/humidity_wf[1-8]/: case ~/humidity[1-8]/: case ~/soilmoisture[1-8]/: case "humi_co2": state.sensor = 1; if (debug) log.debug "Updating humidity: $val" updated = attributeUpdateHumidity(val, "humidity"); if (attributeUpdateDewPoint(val, "dewPoint", "humidityAbs")) updated = true; if (attributeUpdateHeatIndex(val, "heatIndex", "heatDanger", "heatColor")) updated = true; if (attributeUpdateSimmerIndex(val, "simmerIndex", "simmerDanger", "simmerColor")) updated = true; break; case ~/baromrelin_wf[1-8]/: case "baromrelin": state.sensor = 1; // we ignore this value as we do our own correction break; case ~/baromabsin_wf[1-8]/: case "baromabsin": state.sensor = 1; updated = attributeUpdatePressure(val, "pressure", "pressureAbs"); break; case ~/rainratein_wf[1-8]/: case "rainratein": case "rrain_piezo": state.sensor = 1; state.sensorRain = 1; updated = attributeUpdateRain(val, "rainRate", true); break; case ~/eventrainin_wf[1-8]/: case "eventrainin": case "erain_piezo": state.sensor = 1; state.sensorRain = 1; updated = attributeUpdateRain(val, "rainEvent"); break; case ~/hourlyrainin_wf[1-8]/: case "hourlyrainin": state.sensor = 1; updated = attributeUpdateRain(val, "rainHourly"); break; case ~/dailyrainin_wf[1-8]/: case "dailyrainin": case "drain_piezo": state.sensor = 1 state.sensorRain = 1; updated = attributeUpdateRain(val, "rainDaily"); break; case ~/weeklyrainin_wf[1-8]/: case "weeklyrainin": case "wrain_piezo": state.sensor = 1; state.sensorRain = 1; updated = attributeUpdateRain(val, "rainWeekly"); break; case ~/monthlyrainin_wf[1-8]/: case "monthlyrainin": case "mrain_piezo": state.sensor = 1; state.sensorRain = 1; updated = attributeUpdateRain(val, "rainMonthly"); break; case ~/yearlyrainin_wf[1-8]/: case "yearlyrainin": case "yrain_piezo": state.sensor = 1; state.sensorRain = 1; updated = attributeUpdateRain(val, "rainYearly"); break; case ~/totalrainin_wf[1-8]/: case "totalrainin": case "train_piezo": case "totalainin": state.sensor = 1; state.sensorRain = 1; updated = attributeUpdateRain(val, "rainTotal"); break; case ~/pm25_ch[1-4]/: case "pm25_co2": state.sensor = 1; updated = attributeUpdatePM(val, "pm25"); if (attributeUpdateAQI(val, true, "aqi", "aqiDanger", "aqiColor")) updated = true; break; case ~/pm25_avg_24h_ch[1-4]/: case "pm25_24h_co2": state.sensor = 1; updated = attributeUpdatePM(val, "pm25_avg_24h"); if (attributeUpdateAQI(val, true, "aqi_avg_24h", "aqiDanger_avg_24h", "aqiColor_avg_24h")) updated = true; break; case "pm10_co2": state.sensor = 1; updated = attributeUpdatePM(val, "pm10"); if (attributeUpdateAQI(val, false, "aqi", "aqiDanger", "aqiColor")) updated = true; break; case "pm10_24h_co2": state.sensor = 1; updated = attributeUpdatePM(val, "pm10_avg_24h"); if (attributeUpdateAQI(val, false, "aqi_avg_24h", "aqiDanger_avg_24h", "aqiColor_avg_24h")) updated = true; break; case "co2": state.sensor = 1; updated = attributeUpdateCarbonDioxide(val, "carbonDioxide"); break; case "co2_24h": state.sensor = 1; updated = attributeUpdateCarbonDioxide(val, "carbonDioxide_avg_24h"); break; case ~/leak_ch[1-4]/: state.sensor = 1; updated = attributeUpdateLeak(val, "water", "waterMsg", "waterColor"); break; case ~/lightning_wf[1-8]/: case "lightning": state.sensor = 1; updated = attributeUpdateLightningDistance(val, "lightningDistance"); break; case ~/lightning_num_wf[1-8]/: case "lightning_num": state.sensor = 1; updated = attributeUpdateLightningCount(val, "lightningCount"); break; case ~/lightning_time_wf[1-8]/: case "lightning_time": state.sensor = 1; updated = attributeUpdateLightningTime(val, "lightningTime"); break; case ~/lightning_energy_wf[1-8]/: state.sensor = 1; updated = attributeUpdateLightningEnergy(val, "lightningEnergy"); break; case ~/uv_wf[1-8]/: case "uv": state.sensor = 1; updated = attributeUpdateUV(val, "ultravioletIndex", "ultravioletDanger", "ultravioletColor"); break; case ~/solarradiation_wf[1-8]/: case "solarradiation": state.sensor = 1; updated = attributeUpdateLight(val, "solarRadiation", "illuminance"); break; case ~/winddir_wf[1-8]/: case "winddir": state.sensor = 1; state.sensorWind = 1; updated = attributeUpdateWindDirection(val, "windDirection", "windCompass"); break; case ~/winddir_avg10m_wf[1-8]/: case "winddir_avg10m": state.sensor = 1; state.sensorWind = 1; updated = attributeUpdateWindDirection(val, "windDirection_avg_10m", "windCompass_avg_10m"); break; case ~/windspeedmph_wf[1-8]/: case "windspeedmph": state.sensor = 1; state.sensorWind = 1; updated = attributeUpdateWindSpeed(val, "windSpeed"); if (attributeUpdateWindChill(val, "windChill", "windDanger", "windColor")) updated = true; break; case ~/windspdmph_avg10m_wf[1-8]/: case "windspdmph_avg10m": state.sensor = 1; state.sernsorWind = 1; updated = attributeUpdateWindSpeed(val, "windSpeed_avg_10m"); break; case ~/windgustmph_wf[1-8]/: case "windgustmph": state.sensor = 1; state.sensorWind = 1; updated = attributeUpdateWindSpeed(val, "windGust"); break; case ~/maxdailygust_wf[1-8]/: case "maxdailygust": state.sensor = 1; state.sensorWind = 1; updated = attributeUpdateWindSpeed(val, "windGustMaxDaily"); break; // // End Of Data // case "endofdata": if (updateSensorStatus(bundled)) { // Sensor or part the PWS bundle is not receiving data if (!ztatusIsError()) ztatus("Orphaned", "orange"); } else { // Sensor or all parts of the PWS bundle are receiving data if (!ztatusIsError()) ztatus("OK", "green"); // If we are a bundled PWS sensor, at the endofdata we update the "virtual" battery with the lowest of all the "physical" batteries if (bundled) updated = attributeUpdateLowestBattery(); // Update templates if any if (attributeUpdateHtml("htmlTemplate", "html")) updated = true; } break; default: logDebug("Unrecognized attribute: ${key} = ${val}"); break; } return (updated); } // ------------------------------------------------------------- Boolean updateSensorStatus(bundled) { Boolean orphaned = false; if (debug) log.debug "In update sensors status bundled - $bundled" if (bundled) { if (state.sensorTemp != null) { if (debug) log.debug "In update temp sensor" if (state.sensorTemp == 0) orphaned = true; attributeUpdateString(state.sensorTemp? "false": "true", "orphanedTemp"); state.sensorTemp = 0; } if (state.sensorRain != null) { if (debug) log.debug "in update rain sensor" if (state.sensorRain == 0) orphaned = true; attributeUpdateString(state.sensorRain? "false": "true", "orphanedRain"); state.sensorRain = 0; } if (state.sensorWind != null) { if (debug) log.debug "in update wind sensor" if (state.sensorWind == 0) orphaned = true; attributeUpdateString(state.sensorWind? "false": "true", "orphanedWind"); state.sensorWind = 0; } } else { if (state.sensor != null) { if (debug) log.debug "in update ${state.sensor} sensor" if (state.sensor == 0) orphaned = true; attributeUpdateString(state.sensor? "false": "true", "orphaned"); } } if (state.sensor != null) state.sensor = 0; return (orphaned); } // HTML templates -------------------------------------------------------------------------------------------------------------- private Object htmlGetRepository() { // // Return an Object containing all the templates // or null if something went wrong // Object repository = null; try { // String repositoryText = "https://mircolino.github.io/ecowitt/ecowitt.json".toURL().getText(); String repositoryText = "http://mail.lgk.com/ecowitt1.css".toURL().getText(); // log.debuug "got json = $repositoryText" if (repositoryText) { // text -> json Object parser = new groovy.json.JsonSlurper(); repository = parser.parseText(repositoryText); } } catch (Exception e) { logError("Exception in versionUpdate(): ${e}"); } return (repository); } // ------------------------------------------------------------ private Integer htmlCountAttributes(String htmlAttrib) { // // Return the number of html attributes the driver has // Integer count = 0; // Get a list of all attributes (present/null or not) List attribDrv = attributeEnumerate(false); String attrib; for (Integer idx = 0; idx < 16; idx++) { attrib = idx? "${htmlAttrib}${idx}": htmlAttrib; if (attribDrv.contains(attrib) == false) break; count++; } return (count); } // ------------------------------------------------------------ private Boolean htmlSetAttributes(String val, String htmlAttrib, Integer count, Boolean onlyPresent) { Boolean updated = false; String attrib; for (Integer idx = 0; idx < count; idx++) { attrib = idx? "${htmlAttrib}${idx}": htmlAttrib; if (onlyPresent == false || device.currentValue(attrib) != null) { if (attributeUpdateString(val, attrib)) updated = true; } } return (updated); } // ------------------------------------------------------------ private Integer htmlValidateTemplate(String htmlTempl, String htmlAttrib, Integer count) { // // Return <0) number of invalid attributes in "htmlTempl" // >=0) number of valid attributes in "htmlTempl" // Template is valid only if return > 0 // String pattern = /\$\{([^}]+)\}/; // Build a list of valid attributes names excluding the null ones and ourself (for obvious reasons) List attribDrv = attributeEnumerate(); String attrib; for (Integer idx = 0; idx < count; idx++) { attrib = idx? "${htmlAttrib}${idx}": htmlAttrib; attribDrv.remove(attrib); } // Go through all the ${attribute} expressions in the htmlTempl and collect both good and bad ones List attribOk = []; List attribErr = []; htmlTempl.findAll(~pattern) { java.util.ArrayList match -> attrib = match[1].trim(); if (attribDrv.contains(attrib)) attribOk.add(attrib); else attribErr.add(attrib); } if (attribErr.size() != 0) return (-attribErr.size()); return (attribOk.size()); } // ------------------------------------------------------------ private List htmlGetUserInput(String input, Integer count) { // // Return null if user input is null or empty // Return empty list if user input is invalid: template(s) not found, duplicates, too many, etc. // Otherwise return a list of (unvalidated) templates entered by the user // if (!input) return (null); List templateList = []; if (input.find(/[<>{};:=\'\"#&\$]/)) { // If input has at least one typical html character, then it's a real template templateList.add(input); } else { // Input is an array of repository template IDs List idList = input.tokenize("[, ]"); if (idList) { // We found at least one template ID in the user input, make sure they are not too many Object repository = htmlGetRepository(); if (repository) { Boolean metric = unitSystemIsMetric(); for (Integer idx = 0; idx < idList.size(); idx++) { // Try first the normal templates input = repository.templates."${idList[idx]}"; // If not found try the unit templates if (!input) input = metric? repository.templatesMetric."${idList[idx]}": repository.templatesImperial."${idList[idx]}"; // If still not found, or already found, or exceeded number of templates, return error if (!input || templateList.contains(input) || templateList.size() == count) return ([]); // Good one, let's add it templateList.add(input); } } } } return (templateList); } // ------------------------------------------------------------ private String htmlUpdateUserInput(String input) { // // Return true if HTML templates have been pre-processed sucesfully // String htmlTemplate = "htmlTemplate"; String htmlAttrib = "html"; String template; // Get the maximum number of supported templates Integer count = htmlCountAttributes(htmlAttrib); if (!count) { // Return if we do not support HTML templates return (""); } // Cleanup previous states htmlSetAttributes("n/a", htmlAttrib, count, true); for (Integer idx = 0; idx < count; idx++) { template = idx? "${htmlTemplate}${idx}": htmlTemplate; if (device.getDataValue(template)) device.updateDataValue(template, null); device.data.remove(template); } // Parse user input List templateList = htmlGetUserInput(input, count); if (templateList == null) { // Templates are disabled/empty return (""); } if (templateList.size() == 0) { // Invalid user input return ("Invalid template(s) id, count or repetition"); } for (Integer idx = 0; idx < templateList.size(); idx++) { // We have valid templates: let's validate them if (htmlValidateTemplate(templateList[idx], htmlAttrib, count) < 1) { // Invalid or no attribute in template return ("Invalid attribute or template for the current sensor"); } } // Finally! We have a (1 <= number <= count) of valid templates: let's write them down for (Integer idx = 0; idx < templateList.size(); idx++) { template = idx? "${htmlTemplate}${idx}": htmlTemplate; device.updateDataValue(template, templateList[idx]); } htmlSetAttributes("pending", htmlAttrib, templateList.size(), false); return (""); } // Driver lifecycle ----------------------------------------------------------------------------------------------------------- void installed() { try { logDebug("addedSensor(${device.getDeviceNetworkId()})"); } catch (Exception e) { logError("Exception in installed(): ${e}"); } } // ------------------------------------------------------------ void updated() { try { sendEvent(name: "temperatureChange", value: 0.00) sendEvent(name: "humidityChange",value: 0.00) if (debug) { log.debug "Turning off logging in 1/2 hour!" runIn(1800,logsOff) } Boolean bundled = device.getDataValue("isBundled") if (bundled == null) { sendEvent(name: "bundled", value: "false"); } else { sendEvent(name: "bundled", value: "true"); } // Clear previous states state.clear(); // Pre-process HTML templates (if any) String error = htmlUpdateUserInput(settings.htmlTemplate as String); if (error) ztatus(error, "red"); else ztatus("OK", "green"); } catch (Exception e) { logError("Exception in updated(): ${e}"); } } // ------------------------------------------------------------ void uninstalled() { try { // Notify the parent we are being deleted getParent().uninstalledChildDevice(device.getDeviceNetworkId()); logDebug("deletedSensor(${device.getDeviceNetworkId()})"); } catch (Exception e) { logError("Exception in uninstalled(): ${e}"); } } // ------------------------------------------------------------ void parse(String msg) { try { } catch (Exception e) { logError("Exception in parse(): ${e}"); } } def logsOff() { log.debug "Turning off Logging!" device.updateSetting("debug",[value:"false",type:"bool"]) } // Recycle bin ---------------------------------------------------------------------------------------------------------------- /* */ // EOF ------------------------------------------------------------------------------------------------------------------------