/**
* 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 ------------------------------------------------------------------------------------------------------------------------