/** * Driver: Ecowitt RF Sensor * Author: Simon Burke (Original author Mirco Caramori - github.com/mircolino) * Repository: https://github.com/sburke781/ecowitt * Import URL: https://raw.githubusercontent.com/sburke781/ecowitt/main/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 */ public static String gitHubUser() { return "sburke781"; } public static String gitHubRepo() { return "ecowitt"; } public static String gitHubBranch() { return "main"; } metadata { definition(name: "Ecowitt RF Sensor", namespace: "ecowitt", author: "Simon Burke", importUrl: "https://raw.githubusercontent.com/${gitHubUser()}/${gitHubRepo()}/${gitHubBranch()}/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 "Carbon Dioxide 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 "batterySolar", "number"; // Only created/used for WS80/WS90, tracks capicator battery attribute "batterySolarIcon", "number"; attribute "batterySolarOrg", "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 "carbonDioxide", "number"; // ppm - CO₂ concetration - current attribute "carbonDioxide_avg_24h", "number"; // ppm - CO₂ 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 "leafWetness", "number"; // 0-100% leaf wetness 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 "firmware", "string"; // Used with sensors that have firmware 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 // command "settingsResetConditional"; // Used for backward compatibility to reset device conditional preferences } preferences { input(name: "htmlEnabled", type: "bool", title: "Enable Tile HTML", description: "Rich multi-attribute dashboard tiles using html templates", defaultValue: true); if (htmlEnabled || htmlEnabled == null) { input(name: "htmlTemplate", type: "string", title: "Tile HTML Template(s)", description: "See documentation for input formats", defaultValue: ""); } if (localAltitude != null) { input(name: "localAltitude", type: "string", title: "Altitude to Correct Sea Level Pressure", description: "Examples: \"378 ft\" or \"115 m\"", required: true); } if (voltageMin != null) { input(name: "voltageMin", type: "string", title: "Empty Battery Voltage", description: "Sensor value when battery is empty", required: true); input(name: "voltageMax", type: "string", title: "Full Battery Voltage", description: "Sensor value when battery is full", 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"); } 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"); } 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"); } 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"); } if (decsTemperature != null) { input(name: "decsTemperature", type: "number", title: "Temperature decimals", description: "Enter a single digit number or -1 for no rounding"); } if (decsPressure != null) { input(name: "decsPressure", type: "number", title: "Pressure decimals", description: "Enter a single digit number or -1 for no rounding"); } } } /* * 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); } // Device Status -------------------------------------------------------------------------------------------------------------- private Boolean devStatus(String str = null, String color = null) { if (str) { if (color) str = "${str}"; return (attributeUpdateString(str, "status")); } if (device.currentValue("status") != null) { device.deleteCurrentState("status"); return (true); } return (false); } // ------------------------------------------------------------ private Boolean devStatusIsError() { 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 // def isM = parent.unitSystemIsMetric() if (isM == null) return false else return isM } // ------------------------------------------------------------ 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 (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 void attributeDeleteStale() { if (!settings.calcDewPoint) { if (device.currentValue("dewPoint") != null) device.deleteCurrentState("dewPoint"); if (device.currentValue("humidityAbs") != null) device.deleteCurrentState("humidityAbs"); } if (!settings.calcHeatIndex) { if (device.currentValue("heatIndex") != null) device.deleteCurrentState("heatIndex"); if (device.currentValue("heatDanger") != null) device.deleteCurrentState("heatDanger"); if (device.currentValue("heatColor") != null) device.deleteCurrentState("heatColor"); } if (!settings.calcSimmerIndex) { if (device.currentValue("simmerIndex") != null) device.deleteCurrentState("simmerIndex"); if (device.currentValue("simmerDanger") != null) device.deleteCurrentState("simmerDanger"); if (device.currentValue("simmerColor") != null) device.deleteCurrentState("simmerColor"); } if (!settings.calcWindChill) { if (device.currentValue("windChill") != null) device.deleteCurrentState("windChill"); if (device.currentValue("windDanger") != null) device.deleteCurrentState("windDanger"); if (device.currentValue("windColor") != null) device.deleteCurrentState("windColor"); } if (!settings.htmlEnabled) { if (device.currentValue("batteryIcon") != null) device.deleteCurrentState("batteryIcon"); if (device.currentValue("batteryTempIcon") != null) device.deleteCurrentState("batteryTempIcon"); if (device.currentValue("batteryRainIcon") != null) device.deleteCurrentState("batteryRainIcon"); if (device.currentValue("batteryWindIcon") != null) device.deleteCurrentState("batteryWindIcon"); if (device.currentValue("heatDanger") != null) device.deleteCurrentState("heatDanger"); if (device.currentValue("heatColor") != null) device.deleteCurrentState("heatColor"); if (device.currentValue("simmerDanger") != null) device.deleteCurrentState("simmerDanger"); if (device.currentValue("simmerColor") != null) device.deleteCurrentState("simmerColor"); if (device.currentValue("aqiDanger") != null) device.deleteCurrentState("aqiDanger"); if (device.currentValue("aqiColor") != null) device.deleteCurrentState("aqiColor"); if (device.currentValue("aqiDanger_avg_24h") != null) device.deleteCurrentState("aqiDanger_avg_24h"); if (device.currentValue("aqiColor_avg_24h") != null) device.deleteCurrentState("aqiColor_avg_24h"); if (device.currentValue("waterMsg") != null) device.deleteCurrentState("waterMsg"); if (device.currentValue("waterColor") != null) device.deleteCurrentState("waterColor"); if (device.currentValue("ultravioletDanger") != null) device.deleteCurrentState("ultravioletDanger"); if (device.currentValue("ultravioletColor") != null) device.deleteCurrentState("ultravioletColor"); if (device.currentValue("windDanger") != null) device.deleteCurrentState("windDanger"); if (device.currentValue("windColor") != null) device.deleteCurrentState("windColor"); } } // ------------------------------------------------------------ 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) // 3) voltage solar: range from 0.3V (empty) to 5.3V (full) // BigDecimal original = val.toBigDecimal(); BigDecimal percent; BigDecimal icon; String unitOrg; 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; case 3: // Solar - change range from voltage to (0% - 100%) BigDecimal vMin, vMax; vMin = 0.3; vMax = 5.3; percent = convertRange(original, vMin, vMax, 0, 100); unitOrg = "V"; 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(original, attribBatteryOrg, unitOrg); if (type != 2 || original != 6) { // We are not on USB power if (attributeUpdateNumber(percent, attribBattery, "%", 0)) updated = true; if (settings.htmlEnabled && attributeUpdateNumber(icon, attribBatteryIcon, "%")) 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"; // Get number of decimals (default = 1) Integer decimals = settings.decsTemperature; if (decimals == null) { // First time: initialize and show the preference decimals = 1; device.updateSetting("decsTemperature", [value: decimals, type: "number"]); } // Convert to metric if requested if (unitSystemIsMetric()) { degrees = convert_F_to_C(degrees); measure = "°C"; } return (attributeUpdateNumber(degrees, attribTemperature, measure, decimals)); } // ------------------------------------------------------------ private Boolean attributeUpdateHumidity(String val, String attribHumidity) { BigDecimal percent = val.toBigDecimal(); return (attributeUpdateNumber(percent, attribHumidity, "%", 0)); } // ------------------------------------------------------------ private Boolean attributeUpdateLeafWetness(String val, String attribLeafWetness) { BigDecimal percent = val.toBigDecimal(); return (attributeUpdateNumber(percent, attribLeafWetness, "%", 0)); } // ------------------------------------------------------------ private Boolean attributeUpdatePressure(String val, String attribPressure, String attribPressureAbs) { // Get unit system Boolean metric = unitSystemIsMetric(); // Get number of decimals (default = 2) Integer decimals = settings.decsPressure; if (decimals == null) { // First time: initialize and show the preference decimals = 2; device.updateSetting("decsPressure", [value: decimals, type: "number"]); } // 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, decimals); if (attributeUpdateNumber(absolute, attribPressureAbs, val, decimals)) 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"; } 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; } // lgk set airQualityIndex only if actual aqi not avg if (attribAqi == "aqi") attributeUpdateNumber(aqi, "airQualityIndex", "AQI"); Boolean updated = attributeUpdateNumber(aqi, attribAqi, "AQI"); if (settings.htmlEnabled) { 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"; } if (attributeUpdateString(danger, attribAqiDanger)) updated = true; if (attributeUpdateString(color, attribAqiColor)) updated = true; } return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateCO2(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; Boolean updated = attributeUpdateString(leak? "wet": "dry", attribWater); if (settings.htmlEnabled) { String message, color; if (leak) { message = "Leak detected!"; color = "ff0000"; } else { message = "Dry"; color = "ffffff"; } 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()) { 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"; 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(); Boolean updated = attributeUpdateNumber(index, attribUvIndex, "uvi"); if (settings.htmlEnabled) { 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"; } 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"; } 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; if (!settings.calcDewPoint) { // First time: initialize and show the preference if (settings.calcDewPoint == null) device.updateSetting("calcDewPoint", [value: false, type: "bool"]); } else { BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature != null) { 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 double rH = val.toDouble(); double tC = temperature.doubleValue(); /* //The next step is to obtain the saturation vapor pressure(Es) using formula (5) as before when air temperature is known. double Es = 6.11 * Math.pow(10, 7.5*tC/(237.7+tC)); //The next step is to use the saturation vapor pressure and the relative humidity to compute the actual vapor pressure(E) of the air. This can be done with the following formula. double E = (rH*Es) / 100; //RH=relative humidity of air expressed as a percent.(i.e. 80%) //Now you are ready to use the following formula to obtain the dewpoint temperature. // Note: ln( ) means to take the natural log of the variable in the parentheses BigDecimal degrees = (-430.22 + (237.7 * Math.log(E))) / (-Math.log(E) + 19.08); log.debug("rH = " + rH + ", tC = " + tC + ", Es = " + Es + ", E = " + E + ", degrees = " + degrees); */ // Calculate saturation vapor pressure in millibars double 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 *= rH / 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)) * rH * 2.1674)) / (tC + 273.15); // convert back to Fahrenheit, the attribUpdateTemperature does the final conversion back to C degrees = convert_C_to_F(degrees); if (!unitSystemIsMetric()) { 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; if (!settings.calcHeatIndex) { // First time: initialize and show the preference if (settings.calcHeatIndex == null) device.updateSetting("calcHeatIndex", [value: false, type: "bool"]); } else { BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature != null) { 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; if (temperature < 80) degrees = temperature; 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))); } updated = attributeUpdateTemperature(degrees.toString(), attribHeatIndex); if (settings.htmlEnabled) { String danger; String color; if (temperature < 80) { danger = "Safe"; color = "ffffff"; } else { 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"; } } 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; if (!settings.calcSimmerIndex) { // First time: initialize and show the preference if (settings.calcSimmerIndex == null) device.updateSetting("calcSimmerIndex", [value: false, type: "bool"]); } else { BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature != null) { 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; if (temperature < 70) degrees = temperature; else { BigDecimal humidity = val.toBigDecimal(); degrees = 1.98 * (temperature - (0.55 - (0.0055 * humidity)) * (temperature - 58.0)) - 56.83; } updated = attributeUpdateTemperature(degrees.toString(), attribSimmerIndex); if (settings.htmlEnabled) { String danger; String color; if (temperature < 70) { danger = "Cool"; color = "ffffff"; } else { 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"; } } 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; if (!settings.calcWindChill) { // First time: initialize and show the preference if (settings.calcWindChill == null) device.updateSetting("calcWindChill", [value: false, type: "bool"]); } else { BigDecimal temperature = (device.currentValue("temperature") as BigDecimal); if (temperature != null) { 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; BigDecimal windSpeed = val.toBigDecimal(); if (temperature > 50 || windSpeed < 3) degrees = temperature; else degrees = 35.74 + (0.6215 * temperature) - (35.75 * (windSpeed ** 0.16)) + ((0.4275 * temperature) * (windSpeed ** 0.16)); updated = attributeUpdateTemperature(degrees.toString(), attribWindChill); if (settings.htmlEnabled) { String danger; String color; if (temperature > 50 || windSpeed < 3) { danger = "Safe"; color = "ffffff"; } else { 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"; } } if (attributeUpdateString(danger, attribWindDanger)) updated = true; if (attributeUpdateString(color, attribWindColor)) updated = true; } } } return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateFirmware(String val, String attribFirmware) { Boolean updated = attributeUpdateString(val, attribFirmware); return (updated); } // ------------------------------------------------------------ private Boolean attributeUpdateHtml(String templHtml, String attribHtml) { Boolean updated = false; if (settings.htmlEnabled) { 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"); Boolean orphaned = false; 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": case "wh90batt": 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": state.sensor = 1; updated = attributeUpdateBattery(val, "battery", "batteryIcon", "batteryOrg", 0); // !boolean break; case ~/batt_wf[1-8]/: case ~/leaf_batt[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 "ws80cap_volt": case "ws90cap_volt": state.sensor = 1; updated = attributeUpdateBattery(val, "batterySolar", "batterySolarIcon", "batterySolarOrg", 3); break; case "tempinf": // We set this here because it's the integrated GW1000 sensor, which has no battery state.sensor = 1; case "tempf": case ~/tempf_wf[1-8]/: case ~/temp[1-8]f/: case ~/tf_ch[1-8]/: case "tf_co2": updated = attributeUpdateTemperature(val, "temperature"); break; case "humidityin": case "humidity": case ~/humidity_wf[1-8]/: case ~/humidity[1-8]/: case "humi_co2": 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 ~/soilmoisture[1-8]/: updated = attributeUpdateHumidity(val, "humidity"); break; case ~/leafwetness_ch[1-8]/: updated = attributeUpdateLeafWetness(val, "leafWetness"); break; case ~/baromrelin_wf[1-8]/: case "baromrelin": // we ignore this value as we do our own correction break; case ~/baromabsin_wf[1-8]/: case "baromabsin": updated = attributeUpdatePressure(val, "pressure", "pressureAbs"); break; case ~/rainratein_wf[1-8]/: case "rainratein": case "rrain_piezo": updated = attributeUpdateRain(val, "rainRate", true); break; case ~/eventrainin_wf[1-8]/: case "eventrainin": case "erain_piezo": updated = attributeUpdateRain(val, "rainEvent"); break; case ~/hourlyrainin_wf[1-8]/: case "hourlyrainin": case "hrain_piezo": updated = attributeUpdateRain(val, "rainHourly"); break; case ~/dailyrainin_wf[1-8]/: case "dailyrainin": case "drain_piezo": updated = attributeUpdateRain(val, "rainDaily"); break; case ~/weeklyrainin_wf[1-8]/: case "weeklyrainin": case "wrain_piezo": updated = attributeUpdateRain(val, "rainWeekly"); break; case ~/monthlyrainin_wf[1-8]/: case "monthlyrainin": case "mrain_piezo": updated = attributeUpdateRain(val, "rainMonthly"); break; case ~/yearlyrainin_wf[1-8]/: case "yearlyrainin": case "yrain_piezo": updated = attributeUpdateRain(val, "rainYearly"); break; case ~/totalrainin_wf[1-8]/: case "totalrainin": case "train_piezo": updated = attributeUpdateRain(val, "rainTotal"); break; case ~/pm25_ch[1-4]/: case "pm25_co2": 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": 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": updated = attributeUpdatePM(val, "pm10"); if (attributeUpdateAQI(val, false, "aqi", "aqiDanger", "aqiColor")) updated = true; break; case "pm10_24h_co2": updated = attributeUpdatePM(val, "pm10_avg_24h"); if (attributeUpdateAQI(val, false, "aqi_avg_24h", "aqiDanger_avg_24h", "aqiColor_avg_24h")) updated = true; break; case "co2": updated = attributeUpdateCO2(val, "carbonDioxide"); break; case "co2_24h": updated = attributeUpdateCO2(val, "carbonDioxide_avg_24h"); break; case ~/leak_ch[1-4]/: updated = attributeUpdateLeak(val, "water", "waterMsg", "waterColor"); break; case ~/lightning_wf[1-8]/: case "lightning": updated = attributeUpdateLightningDistance(val, "lightningDistance"); break; case ~/lightning_num_wf[1-8]/: case "lightning_num": updated = attributeUpdateLightningCount(val, "lightningCount"); break; case ~/lightning_time_wf[1-8]/: case "lightning_time": updated = attributeUpdateLightningTime(val, "lightningTime"); break; case ~/lightning_energy_wf[1-8]/: updated = attributeUpdateLightningEnergy(val, "lightningEnergy"); break; case ~/uv_wf[1-8]/: case "uv": updated = attributeUpdateUV(val, "ultravioletIndex", "ultravioletDanger", "ultravioletColor"); break; case ~/solarradiation_wf[1-8]/: case "solarradiation": updated = attributeUpdateLight(val, "solarRadiation", "illuminance"); break; case "ws80_ver": case "ws90_ver": updated = attributeUpdateFirmware(val, "firmware"); break; case ~/winddir_wf[1-8]/: case "winddir": updated = attributeUpdateWindDirection(val, "windDirection", "windCompass"); break; case ~/winddir_avg10m_wf[1-8]/: case "winddir_avg10m": updated = attributeUpdateWindDirection(val, "windDirection_avg_10m", "windCompass_avg_10m"); break; case ~/windspeedmph_wf[1-8]/: case "windspeedmph": updated = attributeUpdateWindSpeed(val, "windSpeed"); if (attributeUpdateWindChill(val, "windChill", "windDanger", "windColor")) updated = true; break; case ~/windspdmph_avg10m_wf[1-8]/: case "windspdmph_avg10m": updated = attributeUpdateWindSpeed(val, "windSpeed_avg_10m"); break; case ~/windgustmph_wf[1-8]/: case "windgustmph": updated = attributeUpdateWindSpeed(val, "windGust"); break; case ~/maxdailygust_wf[1-8]/: case "maxdailygust": updated = attributeUpdateWindSpeed(val, "windGustMaxDaily"); break; // // End Of Data: update orphaned status and html attributes // case "endofdata": if (state.sensorTemp != null) { if (state.sensorTemp == 0) orphaned = true; attributeUpdateString(state.sensorTemp? "false": "true", "orphanedTemp"); state.sensorTemp = 0; } if (state.sensorRain != null) { if (state.sensorRain == 0) orphaned = true; attributeUpdateString(state.sensorRain? "false": "true", "orphanedRain"); state.sensorRain = 0; } if (state.sensorWind != null) { if (state.sensorWind == 0) orphaned = true; attributeUpdateString(state.sensorWind? "false": "true", "orphanedWind"); state.sensorWind = 0; } if (state.sensor != null) { if (state.sensor == 0) orphaned = true; attributeUpdateString(state.sensor? "false": "true", "orphaned"); state.sensor = 0; } if (orphaned) { // Sensor or part the PWS bundle is not receiving data if (!devStatusIsError()) devStatus("Orphaned", "orange"); } else { // Sensor or all parts of the PWS bundle are receiving data if (!devStatusIsError()) devStatus(); // 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 HTML templates if any if (attributeUpdateHtml("htmlTemplate", "html")) updated = true; break; default: logDebug("Unrecognized attribute: ${key} = ${val}"); break; } return (updated); } // 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://${gitHubUser()}.github.io/ecowitt/html/ecowitt.json".toURL().getText(); 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 void htmlDeleteAttributes(String htmlAttrib, Integer count) { String attrib; for (Integer idx = 0; idx < count; idx++) { attrib = idx? "${htmlAttrib}${idx}": htmlAttrib; if (device.currentValue(attrib) != null) device.deleteCurrentState(attrib); } } // ------------------------------------------------------------ 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: // null) html templates have been disabled // "") user input is empty or valid // "") user input is invalid // String htmlTemplate = "htmlTemplate"; String htmlAttrib = "html"; // Delete old data templates (if any) for (Integer idx = 0; idx < 16; idx++) { device.removeDataValue(idx? "${htmlTemplate}${idx}": htmlTemplate); } // Get the maximum number of supported templates Integer count = htmlCountAttributes(htmlAttrib); if (!count) { // Return if we do not support HTML templates return (null); } // Cleanup previous states and data htmlDeleteAttributes(htmlAttrib, count); // If templates are disabled we just exit here if (!settings.htmlEnabled) { return (null); } // 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++) { device.updateDataValue(idx? "${htmlTemplate}${idx}": htmlTemplate, templateList[idx]); } return (""); } // Driver Commands ------------------------------------------------------------------------------------------------------------ void settingsResetConditional() { device.removeSetting("localAltitude"); device.removeSetting("voltageMin"); device.removeSetting("voltageMax"); device.removeSetting("calcDewPoint"); device.removeSetting("calcHeatIndex"); device.removeSetting("calcSimmerIndex"); device.removeSetting("calcWindChill"); } // Driver lifecycle ----------------------------------------------------------------------------------------------------------- void installed() { try { logDebug("addedSensor(${device.getDeviceNetworkId()})"); } catch (Exception e) { logError("Exception in installed(): ${e}"); } } // ------------------------------------------------------------ void updated() { try { // Clear previous states and sttributes state.clear(); attributeDeleteStale(); // Pre-process HTML templates (if any) String error = htmlUpdateUserInput(settings.htmlTemplate as String); if (error) devStatus(error, "red"); else devStatus(); } 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}"); } } // Recycle bin ---------------------------------------------------------------------------------------------------------------- /* private Integer attributeDelete(String attrib = null) { // // Delete the specified attribute or all if !attrib // Return the number of deleted attributes // Integer deleted = 0; List list = device.getSupportedAttributes(); if (list) { list.each { if ((!attrib || attrib == it.name) && device.currentValue(it.name) != null) { device.deleteCurrentState(it.name); deleted++; } } } return (deleted); } */ // EOF ------------------------------------------------------------------------------------------------------------------------