/**
* 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 "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 "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
//
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 (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)
//
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;
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() == 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";
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();
// 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);
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;
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 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":
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 ~/leaf_batt[1-8]/:
state.sensor = 1;
updated = attributeUpdateBattery(val, "battery", "batteryIcon", "batteryOrg", 1); // voltage
break;
case "wh25batt":
case "wh65batt":
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":
// 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":
updated = attributeUpdateRain(val, "rainRate", true);
break;
case ~/eventrainin_wf[1-8]/:
case "eventrainin":
updated = attributeUpdateRain(val, "rainEvent");
break;
case ~/hourlyrainin_wf[1-8]/:
case "hourlyrainin":
updated = attributeUpdateRain(val, "rainHourly");
break;
case ~/dailyrainin_wf[1-8]/:
case "dailyrainin":
updated = attributeUpdateRain(val, "rainDaily");
break;
case ~/weeklyrainin_wf[1-8]/:
case "weeklyrainin":
updated = attributeUpdateRain(val, "rainWeekly");
break;
case ~/monthlyrainin_wf[1-8]/:
case "monthlyrainin":
updated = attributeUpdateRain(val, "rainMonthly");
break;
case ~/yearlyrainin_wf[1-8]/:
case "yearlyrainin":
updated = attributeUpdateRain(val, "rainYearly");
break;
case ~/totalrainin_wf[1-8]/:
case "totalrainin":
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 ~/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 ------------------------------------------------------------------------------------------------------------------------