DEPRECATED
Code no longer maintained.
APIXU was acquired by WeatherStack and no longer offers the Free resources this code was based upon.
/***********************************************************************************************************************
* Import URL: https://raw.githubusercontent.com/HubitatCommunity/wx-ApiXU/master/wx-ApiXU-Driver.groovy
* Copyright 2018 CSteele
*
* CLONED from Bangali's ApiXU Weather Driver (v5.4.1)
*
***********************************************************************************************************************/
public static String version() { return "v1.4.6" }
/***********************************************************************************************************************
*
* Version: 1.4.6
* Updated image Icon library location.
* Made all 'wind' related attributes use .toBigDecimal
*
* Version: 1.4.5
* Improved updateCheck() with Switch/Case.
*
* Version: 1.4.4
* Increased Lux 'slices of a day' to include the next day
* Increased pollSunRiseSet to every 8 hours (3 times a day)
* Used "cityName" override option everywhere obs.location.name is used.
*
* Version: 1.4.3
* Change "float" values to BigDecimal or Integer.
* 'Name' and 'City' are duplicates, but made them display the same text.
*
* Version: 1.4.2
* Skip forecastPrecip and mytile data calculations if the user hasn't enabled them.
*
* Version: 1.4.1
* Corrected typo for forecastIcon (in getWUIconName)
*
* Version: 1.4.0
* Moved the schedule() statements to initialize() so they run on reboot too.
* Distributed forecastPrecip() into sunRiseSetHandler, for the once a day portion;
* and into calcTime and doPoll for the display of data.
* Reworked updateLux() to use a schedule() vs chained runIn for robustness.
* Moved sunRiseSet map from 'state' to 'data' storage to declutter State Variables.
* Removed display: true from all sendEvent lines. Hubitat doesn't use it.
*
* Version: 1.3.1
* Corrected typos on twilight (astro vs civil)
*
* Version: 1.3.0
* Added attribute betwixt for Dashboard.
* Rewrote Lux calculation using Milliseconds vs Date Object
* to be half the number of conversions.
*
* Version: 1.2.1
* Made repeating updateLux() run slower at night with lowLuxEvery.
* converted Cobra's updateCheck to Async.
*
* Version: 1.2.0
* Update Attributes to send correct type (Number, String)
* - potential to break user's automation.
*
* Version: 1.1.9
* Update Attributes for the defined Capabilities (no longer selectable).
*
* Version: 1.1.8
* Version: 1.1.7
* Correction for "java.lang.ClassCastException" found by halfrican.ak.
*
* Version: 1.1.6
* Semaphore protecting sunrise/set poll if Apixu poll is incomplete.
* Reworked log.warn messages for uniformity.
* Merged Latitude/Longitude into a group selector.
*
* Version: 1.1.5
* Merged imgNames & conditionFactor Maps into: imgCondMap
*
* Version: 1.1.4
* rewrote handler for sunrise-sunset.org to fill state.sunRiseSet, which gets used
* throughout the day.
* Added "wipe" commands to ease upgrading.
*
* Version: 1.1.3 Thanks @ codahq
* removed the child devices because they aren't needed anymore.
* changed precipitation map variables from "in" (reserved groovy word) to "inches".
* put log.info behind a preference
*
* Version: 1.1.2
* prevent calculating sunrise, sunset, twilight, noon, etc. a hundred times a day.
* added 1 and 3 hour options on Poll() - allowing RM for poll-on-demand.
* converted SunriseAndSet to asynchttp call
*
* Version: 1.1.1
* removed 'configure' as a command, refresh & poll are adequate.
* reorganized attributes into relationship groups with a single selector.
*
* Version: 1.0.0
* renamed wx-ApiXU-Driver.
* reworked Poll and UpdateLux to use common code.
* reworked metadata to build the attributes needed.
* converted Poll to asynchttp call.
* duplicated attributes for OpenWX compatibility with Dashboard Weather Template.
*
/***********************************************************************************************************************
/***********************************************************************************************************************
* Copyright 2018 bangali
*
* Contributors:
* https://github.com/jebbett code for new weather icons based on weather condition data
* https://www.deviantart.com/vclouds/art/VClouds-Weather-Icons-179152045 new weather icons courtesy of VClouds
* https://github.com/arnbme code for mytile
*
* 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.
*
* ApiXU Weather Driver
*
* Author: bangali
*
* Date: 2018-05-27
*
* attribution: weather data courtesy: https://www.apixu.com/
*
* attribution: sunrise and sunset courtesy: https://sunrise-sunset.org/
*
* for use with HUBITAT so no tiles
*
* features:
* - supports global weather data with free api key from apixu.com
* - provides calculated illuminance data based on time of day and weather condition code.
* - no local server setup needed
* - no personal weather station needed
*
*
* record of Bangali's version history prior to the Clone moved to the end of file.
*/
import groovy.transform.Field
metadata {
definition (name: "wx-ApiXU-Driver", namespace: "csteele", author: "bangali, csteele", importUrl: "https://raw.githubusercontent.com/HubitatCommunity/wx-ApiXU/master/wx-ApiXU-Driver.groovy") {
capability "Actuator"
capability "Sensor"
capability "Polling"
capability "Illuminance Measurement"
capability "Temperature Measurement"
capability "Relative Humidity Measurement"
capability "Pressure Measurement"
capability "Ultraviolet Index"
attributesMap.each
{
k, v -> if (v.typeof) attribute "${k}", "${v.typeof}"
}
// some attributes are 'doubled' due to spelling differences, such as wind_dir & windDirection
// the additional doubled attributes are added here:
attribute "windDirection", "number" // open_weatherPublish related
attribute "windSpeed", "number" // open_weatherPublish |
attribute "weatherIcons", "string" // open_weatherPublish |
// some attributes are in a 'group' of similar, under a single selector
attribute "precipDayMinus2", "number" // precipExtended related
attribute "precipDayMinus1", "number" // precipExtended |
attribute "precipDay0", "number" // precipExtended |
attribute "precipDayPlus1", "number" // precipExtended |
attribute "precipDayPlus2", "number" // precipExtended |
attribute "local_sunrise", "string" // localSunrisePublish related
attribute "local_sunset", "string" // localSunrisePublish |
attribute "localSunrise", "string" // localSunrisePublish |
attribute "localSunset", "string" // localSunrisePublish |
attribute "lat", "number" // latPublish related
attribute "lon", "number" // latPublish |
attribute "temperatureHighDayPlus1", "number" // tempHiLowPublish related
attribute "temperatureLowDayPlus1", "number" // tempHiLowPublish |
attribute "betwixt", "string"
command "refresh"
// command "updateLux" // **---** delete for Release
// command "pollSunRiseSet" // **---** delete for Release
// command "updateCheck" // **---** delete for Release
}
def settingDescr = settingEnable ? "
Hide many of the Preferences to reduce the clutter, if needed, by turning OFF this toggle.
" : "
Many Preferences are available to you, if needed, by turning ON this toggle.
"
preferences {
input "zipCode", "text", title:"Zip code or city name or latitude,longitude?", required:true
input "apixuKey", "text", title:"ApiXU key?", required:true, defaultValue:null
input "cityName", "text", title: "Override default city name?", required:false, defaultValue:null
input "isFahrenheit", "bool", title:"Use Imperial units?", required:true, defaultValue:true
input "pollEvery", "enum", title:"Poll ApiXU how frequently?\nrecommended setting 30 minutes.\nilluminance updating defaults to every 5 minutes.", required:false, defaultValue: 30, options:[5:"5 minutes",10:"10 minutes",15:"15 minutes",30:"30 minutes",60:"1 hour",180:"3 hours"]
input "luxEvery", "enum", title:"Publish illuminance how frequently?", required:false, defaultValue: 5, options:[5:"5 minutes",10:"10 minutes",15:"15 minutes",30:"30 minutes"]
input "lowLuxEvery", "enum", title:"When illuminance is minimum, how frequently is it published?", required:false, defaultValue: 999, options:[999: "don't change", 5:"5 minutes",10:"10 minutes",15:"15 minutes",30:"30 minutes"]
input "settingEnable", "bool", title: "Display All Preferences", description: "$settingDescr", defaultValue: true
input "debugOutput", "bool", title: "Enable debug logging?", defaultValue: true
input "descTextEnable","bool", title: "Enable descriptionText logging?", defaultValue: true
// build a Selector for each mapped Attribute or group of attributes
attributesMap.each
{
keyname, attribute ->
if (settingEnable) input "${keyname}Publish", "bool", title: "${attribute.title}", required: true, defaultValue: "${attribute.default}", description: "
${attribute.descr}
"
}
}
}
// helpers
def refresh() { poll() }
/*
updated
Purpose: runs when save is clicked in the preferences section
*/
def updated() {
initialize() // includes an unsubscribe()
state.clockSeconds = true
if (debugOutput) runIn(1800,logsOff) // disable debug logs after 30 min
if (settingEnable) runIn(2100,settingsOff) // "roll up" (hide) the condition selectors after 35 min
if (pollEvery == "180") { "runEvery3Hours"(poll) }
else if (pollEvery == "60") { "runEvery1Hour"(poll) }
else { "runEvery${pollEvery}Minutes"(poll) }
if (dashClock) updateClock();
poll()
if (descTextEnable) log.info "Updated with settings: ${settings}, $state.sunRiseSet"
}
/*
doPoll
Purpose: build out the Attributes and add to Hub DB if selected
*/
def doPoll(obs) {
if (descTextEnable) log.info "wx-ApiXU poll for: $zipCode"
calcTime(obs) // calculate all the time variables
sendEvent(name: "lastXUupdate", value: now)
// Update Attributes for the defined Capabilities
sendEvent(name: "humidity", value: obs.current.humidity.toInteger(), unit: "%")
sendEvent(name: "pressure", value: (isFahrenheit ? obs.current.pressure_in.toInteger() : obs.current.pressure_mb.toInteger()), unit: "${(isFahrenheit ? 'IN' : 'MBAR')}")
sendEvent(name: "temperature", value: (isFahrenheit ? obs.current.temp_f.toBigDecimal() : obs.current.temp_c.toBigDecimal()), unit: "${(isFahrenheit ? 'F' : 'C')}")
sendEvent(name: "ultravioletIndex", value: obs.current.uv.toInteger())
if (localSunrisePublish) {
if (debugOutput) log.debug "localSunrise Group"
sendEvent(name: "local_sunrise", value: state.localSunrise, descriptionText: "Sunrise today is at $state.localSunrise")
sendEvent(name: "local_sunset", value: state.localSunset, descriptionText: "Sunset today at is $state.localSunset")
sendEvent(name: "localSunrise", value: state.localSunrise)
sendEvent(name: "localSunset", value: state.localSunset)
}
if (open_weatherPublish) {
if (debugOutput) log.debug "open_weather Group"
sendEvent(name: "weatherIcons", value: getOWIconName(obs.current.condition.code, obs.current.is_day))
sendEvent(name: "windSpeed", value: (isFahrenheit ? obs.current.wind_mph.toBigDecimal() : obs.current.wind_kph.toBigDecimal()))
sendEvent(name: "windDirection", value: obs.current.wind_degree.toInteger())
}
if (tempHiLowPublish) {
if (debugOutput) log.debug "temp+1 Hi/Lo Group"
sendEvent(name: "temperatureHighDayPlus1", value: (isFahrenheit ? obs.forecast.forecastday[0].day.maxtemp_f.toInteger() :
obs.forecast.forecastday[0].day.maxtemp_c.toInteger()), unit: "${(isFahrenheit ? 'F' : 'C')}")
sendEvent(name: "temperatureLowDayPlus1", value: (isFahrenheit ? obs.forecast.forecastday[0].day.mintemp_f.toInteger() :
obs.forecast.forecastday[0].day.mintemp_c.toInteger()), unit: "${(isFahrenheit ? 'F' : 'C')}")
}
if (latPublish) { // latitude and longitude group
if (debugOutput) log.debug "Lat/Long Group"
sendEvent(name: "lat", value: obs.location.lat.toBigDecimal())
sendEvent(name: "lon", value: obs.location.lon.toBigDecimal())
}
sendEventPublish(name: "city", value: (cityName ?: (obs.location.name ?: "n/a")))
sendEventPublish(name: "cloud", value: obs.current.cloud.toInteger(), unit: "%")
sendEventPublish(name: "condition_code", value: obs.current.condition.code.toInteger())
sendEventPublish(name: "condition_codeDayPlus1", value: obs.forecast.forecastday[0].day.condition.code.toInteger())
sendEventPublish(name: "condition_icon_only", value: obs.current.condition.icon.split("/")[-1])
sendEventPublish(name: "condition_icon_url", value: 'https:' + obs.current.condition.icon)
sendEventPublish(name: "condition_icon", value: '')
sendEventPublish(name: "condition_text", value: obs.current.condition.text)
sendEventPublish(name: "country", value: (obs.location.country ?: " "))
sendEventPublish(name: "feelsLike", value: (isFahrenheit ? obs.current.feelslike_f.toBigDecimal() : obs.current.feelslike_c.toBigDecimal()), unit: "${(isFahrenheit ? 'F' : 'C')}")
sendEventPublish(name: "forecastIcon", value: getWUIconName(obs.current.condition.code, obs.current.is_day))
sendEventPublish(name: "is_day", value: obs.current.is_day.toInteger())
sendEventPublish(name: "last_updated_epoch", value: obs.current.last_updated_epoch.toInteger())
sendEventPublish(name: "last_updated", value: obs.current.last_updated)
sendEventPublish(name: "local_date", value: state.thisDate)
sendEventPublish(name: "local_time", value: state.thisTime)
sendEventPublish(name: "localtime_epoch", value: obs.location.localtime_epoch.toInteger())
sendEventPublish(name: "location", value: ((obs.location.name ?: "n/a") + ', ' + (obs.location.region ?: " ")))
sendEventPublish(name: "name", value: (cityName ?: (obs.location.name ?: "n/a")))
sendEventPublish(name: "percentPrecip", value: (isFahrenheit ? obs.current.precip_in.toBigDecimal() : obs.current.precip_mm.toBigDecimal()), unit: "${(isFahrenheit ? 'IN' : 'MM')}")
sendEventPublish(name: "region", value: (obs.location.region ?: " "))
sendEventPublish(name: "twilight_begin", value: state.twiBegin, descriptionText: "Twilight begins today at $state.twiBegin")
sendEventPublish(name: "twilight_end", value: state.twiEnd, descriptionText: "Twilight ends today at $state.twiEnd")
sendEventPublish(name: "tz_id", value: obs.location.tz_id)
sendEventPublish(name: "visual", value: '')
sendEventPublish(name: "visualDayPlus1", value: '')
sendEventPublish(name: "visualDayPlus1WithText", value: '
' + obs.forecast.forecastday[0].day.condition.text)
sendEventPublish(name: "visualWithText", value: '
' + obs.current.condition.text)
sendEventPublish(name: "weather", value: obs.current.condition.text)
sendEventPublish(name: "wind_degree", value: obs.current.wind_degree.toInteger(), unit: "DEGREE")
sendEventPublish(name: "wind_dir", value: obs.current.wind_dir)
sendEventPublish(name: "wind_mytile", value: wind_mytile)
sendEventPublish(name: "wind", value: (isFahrenheit ? obs.current.wind_mph.toBigDecimal() : obs.current.wind_kph.toBigDecimal()), unit: "${(isFahrenheit ? 'MPH' : 'KPH')}")
if (isFahrenheit) {
sendEventPublish(name: "wind_mph", value: obs.current.wind_mph.toBigDecimal(), unit: "MPH")
sendEventPublish(name: "precip_in", value: obs.current.precip_in.toBigDecimal(), unit: "IN")
sendEventPublish(name: "feelslike_f", value: obs.current.feelslike_f.toBigDecimal(), unit: "F")
sendEventPublish(name: "vis_miles", value: obs.current.vis_miles.toBigDecimal(), unit: "MILES")
}
else {
sendEventPublish(name: "wind_kph", value: obs.current.wind_kph.toBigDecimal(), unit: "KPH")
sendEventPublish(name: "wind_mps", value: ((obs.current.wind_kph / 3.6f).round(1).toBigDecimal()), unit: "MPS")
sendEventPublish(name: "precip_mm", value: obs.current.precip_mm.toBigDecimal(), unit: "MM")
sendEventPublish(name: "feelsLike_c", value: obs.current.feelslike_c.toBigDecimal(), unit: "C")
sendEventPublish(name: "vis_km", value: obs.current.vis_km.toInteger(), unit: "KM")
}
if (precipExtendedPublish) {
if (debugOutput) log.debug "Extended Precip Group"
sendEvent(name: "precipDayMinus2", value: (isFahrenheit ? state.forecastPrecip.precipDayMinus2.inch.toBigDecimal() : state.forecastPrecip.precipDayMinus2.mm.toBigDecimal()), unit: "${(isFahrenheit ? 'IN' : 'MM')}")
sendEvent(name: "precipDayMinus1", value: (isFahrenheit ? state.forecastPrecip.precipDayMinus1.inch.toBigDecimal() : state.forecastPrecip.precipDayMinus1.mm.toBigDecimal()), unit: "${(isFahrenheit ? 'IN' : 'MM')}")
sendEvent(name: "precipDay0", value: (isFahrenheit ? state.forecastPrecip.precipDay0.inch.toBigDecimal() : state.forecastPrecip.precipDay0.mm.toBigDecimal()), unit: "${(isFahrenheit ? 'IN' : 'MM')}")
sendEvent(name: "precipDayPlus1", value: (isFahrenheit ? state.forecastPrecip.precipDayPlus1.inch.toBigDecimal() : state.forecastPrecip.precipDayPlus1.mm.toBigDecimal()), unit: "${(isFahrenheit ? 'IN' : 'MM')}")
sendEvent(name: "precipDayPlus2", value: (isFahrenheit ? state.forecastPrecip.precipDayPlus2.inch.toBigDecimal() : state.forecastPrecip.precipDayPlus2.mm.toBigDecimal()), unit: "${(isFahrenheit ? 'IN' : 'MM')}")
}
sendEventPublish(name: "mytile", value: mytext)
return
}
/*
poll
Purpose: initiate the asynchtttpGet() call each poll cycle.
Notes: very, very simple, all the action is in the handler.
*/
def poll() {
def requestParams = [ uri: "https://api.apixu.com/v1/forecast.json?key=$apixuKey&q=$zipCode&days=3" ]
// log.debug "Poll ApiXU: $requestParams"
asynchttpGet("pollHandler", requestParams)
}
/*
pollHandler
Purpose: the APIXU website response
Notes: a good response will be processed by doPoll()
*/
def pollHandler(resp, data) {
if(resp.getStatus() == 200 || resp.getStatus() == 207) {
obs = parseJson(resp.data)
// if (debugOutput) log.debug "wx-ApiXU returned: $obs"
doPoll(obs) // parse the data returned by ApiXU
} else {
log.error "wx-ApiXU weather api did not return data"
}
}
/*
Sun Rise Set
Purpose: Run just after midnight to establish the Astronomical times needed all day long when polling APIXU.
*/
def pollSunRiseSet() {
if (state.loc_lat) {
def requestParams = [ uri: "https://api.sunrise-sunset.org/json?lat=$state.loc_lat&lng=$state.loc_lon&formatted=0" ]
if (state.thisDate) {requestParams = [ uri: "https://api.sunrise-sunset.org/json?lat=$state.loc_lat&lng=$state.loc_lon&formatted=0&date=$state.thisDate" ]}
if (descTextEnable) log.info "SunRiseSet poll for $state.loc_lat $state.loc_lon" //$requestParams"
asynchttpGet("sunRiseSetHandler", requestParams)
} else {
state.sunRiseSet.init = false
log.warn "wx-ApiXU no sunrise-sunset without Lat/Long."
}
}
def sunRiseSetHandler(resp, data) {
if(resp.getStatus() == 200 || resp.getStatus() == 207) {
sunRiseSet = resp.getJson().results
updateDataValue("sunRiseSet", resp.data)
state.sunRiseSet.init = true
state.localSunrise = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXXX", sunRiseSet.sunrise).format("HH:mm")
state.localSunset = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXXX", sunRiseSet.sunset).format("HH:mm")
state.twiBegin = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXXX", sunRiseSet.civil_twilight_begin).format("HH:mm")
state.twiEnd = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXXX", sunRiseSet.civil_twilight_end).format("HH:mm")
state.forecastPrecip.precipDayMinus2 = state?.forecastPrecip?.precipDayMinus1
state.forecastPrecip.precipDayMinus1 = state?.forecastPrecip?.precipDay0
} else {
log.warn "wx-ApiXU sunrise-sunset api did not return data"
state.sunRiseSet.init = false
}
}
private isConfigured() {
getDataValue("configured") == "true"
}
/*
updateLux
Purpose: calculate Lux / Illuminance / Illuminated offset by time of day
Notes: minimum Lux is a value of 5 after dark.
*/
def updateLux() {
if (state?.sunRiseSet?.init) {
if (descTextEnable) log.info "wx-ApiXU lux calc for: $zipCode" // ", $state.loc_lat, $state.localSunset"
def (lux, bwn) = estimateLux(state.condition_code, state.cloud)
state.luxNext = (lux > 6) ? true : false
state.luxNext ? { schedule("0 0/${luxEvery} * * * ?", updateLux) } : {if (lowLuxEvery != 999) { schedule("0 0/${lowLuxEvery} * * * ?", updateLux) } }
//if (debugOutput) log.debug "Lux: $lux, $state.luxNext, $bwn"
sendEvent(name: "illuminance", value: lux.toInteger(), unit: "lux")
sendEventPublish(name: "illuminated", value: String.format("%,d lux", lux))
sendEventPublish(name: "betwixt", value: bwn)
} else {
if (descTextEnable) log.warn "no wx-ApiXU lux without sunRiseSet value."
runIn(2, pollSunRiseSet)
}
}
def estimateLux(condition_code, cloud) {
def lux = 0l
def aFCC = true
def l
def bwn
def sunRiseSet = parseJson(getDataValue("sunRiseSet")).results
def tZ = TimeZone.getTimeZone(state.tz_id)
def lT = new Date().format("yyyy-MM-dd'T'HH:mm:ssXXX", tZ)
def localeMillis = getEpoch(lT)
def twilight_beginMillis = getEpoch(sunRiseSet.civil_twilight_begin)
def sunriseTimeMillis = getEpoch(sunRiseSet.sunrise)
def noonTimeMillis = getEpoch(sunRiseSet.solar_noon)
def sunsetTimeMillis = getEpoch(sunRiseSet.sunset)
def twilight_endMillis = getEpoch(sunRiseSet.civil_twilight_end)
def twiStartNextMillis = twilight_beginMillis + 86400000 // = 24*60*60*1000 --> one day in milliseconds
def sunriseNextMillis = sunriseTimeMillis + 86400000
def noonTimeNextMillis = noonTimeMillis + 86400000
def sunsetNextMillis = sunsetTimeMillis + 86400000
def twiEndNextMillis = twilight_endMillis + 86400000
switch(localeMillis) {
case { it < twilight_beginMillis}:
bwn = "Fully Night Time"
lux = 5l
break
case { it < sunriseTimeMillis}:
bwn = "between twilight and sunrise"
l = (((localeMillis - twilight_beginMillis) * 50f) / (sunriseTimeMillis - twilight_beginMillis))
lux = (l < 10f ? 10l : l.trunc(0) as long)
break
case { it < noonTimeMillis}:
bwn = "between sunrise and noon"
l = (((localeMillis - sunriseTimeMillis) * 10000f) / (noonTimeMillis - sunriseTimeMillis))
lux = (l < 50f ? 50l : l.trunc(0) as long)
break
case { it < sunsetTimeMillis}:
bwn = "between noon and sunset"
l = (((sunsetTimeMillis - localeMillis) * 10000f) / (sunsetTimeMillis - noonTimeMillis))
lux = (l < 50f ? 50l : l.trunc(0) as long)
break
case { it < twilight_endMillis}:
bwn = "between sunset and twilight"
l = (((twilight_endMillis - localeMillis) * 50f) / (twilight_endMillis - sunsetTimeMillis))
lux = (l < 10f ? 10l : l.trunc(0) as long)
break
case { it < twiStartNextMillis}:
bwn = "Fully Night Time"
lux = 5l
break
case { it < sunriseNextMillis}:
bwn = "between twilight and sunrise"
l = (((localeMillis - twiStartNextMillis) * 50f) / (sunriseNextMillis - twiStartNextMillis))
lux = (l < 10f ? 10l : l.trunc(0) as long)
break
case { it < noonTimeNextMillis}:
bwn = "between sunrise and noon"
l = (((localeMillis - sunriseNextMillis) * 10000f) / (noonTimeNextMillis - sunriseNextMillis))
lux = (l < 50f ? 50l : l.trunc(0) as long)
break
case { it < sunsetNextMillis}:
bwn = "between noon and sunset"
l = (((sunsetNextMillis - localeMillis) * 10000f) / (sunsetNextMillis - noonTimeNextMillis))
lux = (l < 50f ? 50l : l.trunc(0) as long)
break
case { it < twiEndNextMillis}:
bwn = "between sunset and twilight"
l = (((twiEndNextMillis - localeMillis) * 50f) / (twiEndNextMillis - sunsetNextMillis))
lux = (l < 10f ? 10l : l.trunc(0) as long)
break
default:
bwn = "Fully Night Time"
lux = 5l
aFCC = false
break
}
def cC = condition_code.toInteger()
def cCF
if (aFCC)
if (imgCondMap[cC].condCode)
{
cCF = imgCondMap[cC].condCode[1]
}
else
{
// factor in cloud cover if available
cCF = state.apixu.init ? ((100 - (cloud.toInteger() / 3d)) / 100) : 0.998d
// log.debug "zzz: $l $cloud $cCF, $lux, $bwn, $state.apixu.init"
}
else
{
cCF = 1.0
}
lux = (lux * cCF) as long
if (debugOutput) log.debug "condition: $cC | condition factor: $cCF | lux: $lux"
sendEventPublish(name: "cCF", value: cCF)
return [lux, bwn]
}
/*
calcTime
Purpose: calculate display data from each observation (aka: obs/wxData).
*/
def calcTime(wxData) {
state.condition_code = wxData.current.condition.code
state.cloud = wxData.current.cloud
// with sun rise/set being async and once a day, 'obs' (wxdata) won't be available
// to that method. Lat + long needs to be saved.
state.loc_lat = wxData.location.lat ?: location.latitude
state.loc_lon = wxData.location.lon ?: location.longitude
state.tz_id = wxData.location.tz_id ?: TimeZone.getDefault().getID()
state.thisDate = Date.parse("yyyy-MM-dd HH:mm", wxData.location.localtime).format("yyyy-MM-dd")
state.thisTime = Date.parse("yyyy-MM-dd HH:mm", wxData.location.localtime).format("HH:mm")
imgName = getImgName(wxData.current.condition.code, wxData.current.is_day)
imgNamePlus1 = getImgName(wxData.forecast.forecastday[0].day.condition.code, 1)
wind_mytile=(isFahrenheit ? "${Math.round(wxData.current.wind_mph)}" + " mph " : "${Math.round(wxData.current.wind_kph)}" + " kph ")
if (mytilePublish) { // don't bother setting these values if it's not enabled
// build the myTile text
mytext = (cityName ?: (wxData.location.name ?: "n/a")) + ', ' + (wxData.location.region ?: " ")
mytext += '
' + (isFahrenheit ? "${Math.round(wxData.current.temp_f)}" + '°F ' : wxData.current.temp_c + '°C ') + wxData.current.humidity + '%'
mytext += '
' + state?.localSunrise + ' ' + state?.localSunset
mytext += (wind_mytile == (isFahrenheit ? "0 mph " : "0 kph ") ? '
Wind is calm' : '
' + wxData.current.wind_dir + ' ' + wind_mytile)
mytext += '
' + wxData.current.condition.text
//if (debugOutput) log.debug "mytext: $mytext"
}
if (precipExtendedPublish) { // don't bother setting these values if it's not enabled
state.forecastPrecip.precipDay0.mm = wxData.forecast.forecastday[0].day.totalprecip_mm
state.forecastPrecip.precipDay0.inch = wxData.forecast.forecastday[0].day.totalprecip_in
state.forecastPrecip.precipDayPlus1.mm = wxData.forecast.forecastday[1].day.totalprecip_mm
state.forecastPrecip.precipDayPlus1.inch = wxData.forecast.forecastday[1].day.totalprecip_in
state.forecastPrecip.precipDayPlus2.mm = wxData.forecast.forecastday[2].day.totalprecip_mm
state.forecastPrecip.precipDayPlus2.inch = wxData.forecast.forecastday[2].day.totalprecip_in
}
}
def logsOff(){
log.warn "debug logging disabled..."
device.updateSetting("debugOutput",[value:"false",type:"bool"])
}
def settingsOff(){
log.warn "Settings disabled..."
device.updateSetting("settingEnable",[value:"false",type:"bool"])
}
/*
sendEventPublish
Purpose: Attribute sent to DB if selected
*/
def sendEventPublish(evt) {
if (this[evt.name + "Publish"]) {
sendEvent(name: evt.name, value: evt.value, descriptionText: evt.descriptionText, unit: evt.unit, displayed: evt.displayed);
if (debugOutput) log.debug "$evt.name" //: $evt.name, $evt.value $evt.unit"
}
}
/*
getEpoch
Purpose: take a Date object and return Milliseconds (Epoch)
Notes:
*/
def getEpoch (aTime) {
def tZZ = TimeZone.getTimeZone(state.tz_id)
def localeTime = new Date().parse("yyyy-MM-dd'T'HH:mm:ssXXX", aTime, tZZ)
long localeMillis = localeTime.getTime()
return (localeMillis)
}
/*
updateClock
Purpose: implements a blinking : in a dashboard clock
*/
def updateClock() {
runIn(2, updateClock)
if (!state.tz_id) return;
def nowTime = new Date()
sendEventPublish(name: "local_time", value: nowTime.format((state.clockSeconds ? "HH:mm" : "HH mm"), location.timeZone))
def localDate = nowTime.format("yyyy-MM-dd", location.timeZone)
if (localDate != state.localDate) {
state.localDate = localDate
sendEventPublish(name: "local_date", value: localDate)
}
state.clockSeconds = (state.clockSeconds ? false : true)
}
/*
getWUIconName
Purpose: get the WeatherUnderground image value.
*/
def getWUIconName(condition_code, is_day) {
def wuIcon = imgCondMap[condition_code].condCode[0] ? imgCondMap[condition_code].condCode[2] : ''
if (is_day != 1 && wuIcon) wuIcon = 'nt_' + wuIcon;
return wuIcon
}
/*
getOWIconName
Purpose: Hubitat's Weather template for Dashboard is OpenWeather icon friendly ONLY.
*/
def getOWIconName(condition_code, is_day) {
def wIcon = imgCondMap[condition_code].condCode[0] ? imgCondMap[condition_code].condCode[3] : ''
return is_day ? wIcon + 'd' : wIcon + 'n'
}
/*
getImgName
Purpose: get our image id.
*/
def getImgName(wCode, is_day) {
def url = "https://raw.githubusercontent.com/HubitatCommunity/wx-ApiXU/master/docs/weather/"
def imgItem = isDay ? imgCondMap[wCode].imgNames.night_img : imgCondMap[wCode].imgNames.day_img
// log.debug "getImgName: $wCode, $imgItem"
return (url + imgItem)
}
@Field static imgCondMap = [
1000: [imgNames: [day_img: '32.png', night_img: '31.png'], condCode: ['Sunny', 1, 'sunny', '01'] ], // Sunny
1003: [imgNames: [day_img: '30.png', night_img: '29.png'], condCode: ['Partly cloudy', 0.8, 'partlycloudy', '03'] ], // Partly cloudy
1006: [imgNames: [day_img: '28.png', night_img: '27.png'], condCode: ['Cloudy', 0.6, 'cloudy', '02'] ], // Cloudy
1009: [imgNames: [day_img: '26.png', night_img: '26.png'], condCode: ['Overcast', 0.5, 'cloudy', '13'] ], // Overcast
1030: [imgNames: [day_img: '20.png', night_img: '20.png'], condCode: ['Mist', 0.5, 'fog', '13'] ], // Mist
1063: [imgNames: [day_img: '39.png', night_img: '45.png'], condCode: ['Patchy rain possible', 0.8, 'chancerain', '04'] ], // Patchy rain possible
1066: [imgNames: [day_img: '41.png', night_img: '46.png'], condCode: ['Patchy snow possible', 0.6, 'chancesnow', '13'] ], // Patchy snow possible
1069: [imgNames: [day_img: '41.png', night_img: '46.png'], condCode: ['Patchy sleet possible', 0.6, 'chancesleet', '13'] ], // Patchy sleet possible
1072: [imgNames: [day_img: '39.png', night_img: '45.png'], condCode: ['Patchy freezing drizzle possible', 0.4, 'chancesleet', '13'] ], // Patchy freezing drizzle possible
1087: [imgNames: [day_img: '38.png', night_img: '47.png'], condCode: ['Thundery outbreaks possible', 0.2, 'chancetstorms', '11'] ], // Thundery outbreaks possible
1114: [imgNames: [day_img: '15.png', night_img: '15.png'], condCode: ['Blowing snow', 0.3, 'snow', '13'] ], // Blowing snow
1117: [imgNames: [day_img: '16.png', night_img: '16.png'], condCode: ['Blizzard', 0.1, 'snow', '13'] ], // Blizzard
1135: [imgNames: [day_img: '21.png', night_img: '21.png'], condCode: ['Fog', 0.2, 'fog', '50'] ], // Fog
1147: [imgNames: [day_img: '21.png', night_img: '21.png'], condCode: ['Freezing fog', 0.1, 'fog', '13'] ], // Freezing fog
1150: [imgNames: [day_img: '39.png', night_img: '45.png'], condCode: ['Patchy light drizzle', 0.8, 'rain', '10'] ], // Patchy light drizzle
1153: [imgNames: [day_img: '11.png', night_img: '11.png'], condCode: ['Light drizzle', 0.7, 'rain', '09'] ], // Light drizzle
1168: [imgNames: [day_img: '8.png', night_img: '8.png'], condCode: ['Freezing drizzle', 0.5, 'sleet', '13'] ], // Freezing drizzle
1171: [imgNames: [day_img: '10.png', night_img: '10.png'], condCode: ['Heavy freezing drizzle', 0.2, 'sleet', '13'] ], // Heavy freezing drizzle
1180: [imgNames: [day_img: '39.png', night_img: '45.png'], condCode: ['Patchy light rain', 0.8, 'rain', '09'] ], // Patchy light rain
1183: [imgNames: [day_img: '11.png', night_img: '11.png'], condCode: ['Light rain', 0.7, 'rain', '09'] ], // Light rain
1186: [imgNames: [day_img: '39.png', night_img: '45.png'], condCode: ['Moderate rain at times', 0.5, 'rain', '09'] ], // Moderate rain at times
1189: [imgNames: [day_img: '12.png', night_img: '12.png'], condCode: ['Moderate rain', 0.4, 'rain', '09'] ], // Moderate rain
1192: [imgNames: [day_img: '39.png', night_img: '45.png'], condCode: ['Heavy rain at times', 0.3, 'rain', '09'] ], // Heavy rain at times
1195: [imgNames: [day_img: '12.png', night_img: '12.png'], condCode: ['Heavy rain', 0.2, 'rain', '09'] ], // Heavy rain
1198: [imgNames: [day_img: '8.png', night_img: '8.png'], condCode: ['Light freezing rain', 0.7, 'sleet', '13'] ], // Light freezing rain
1201: [imgNames: [day_img: '10.png', night_img: '10.png'], condCode: ['Moderate or heavy freezing rain', 0.3, 'sleet', '13'] ], // Moderate or heavy freezing rain
1204: [imgNames: [day_img: '5.png', night_img: '5.png'], condCode: ['Light sleet', 0.5, 'sleet', '13'] ], // Light sleet
1207: [imgNames: [day_img: '6.png', night_img: '6.png'], condCode: ['Moderate or heavy sleet', 0.3, 'sleet', '13'] ], // Moderate or heavy sleet
1210: [imgNames: [day_img: '41.png', night_img: '41.png'], condCode: ['Patchy light snow', 0.8, 'flurries', '13'] ], // Patchy light snow
1213: [imgNames: [day_img: '18.png', night_img: '18.png'], condCode: ['Light snow', 0.7, 'snow', '13'] ], // Light snow
1216: [imgNames: [day_img: '41.png', night_img: '41.png'], condCode: ['Patchy moderate snow', 0.6, 'snow', '13'] ], // Patchy moderate snow
1219: [imgNames: [day_img: '16.png', night_img: '16.png'], condCode: ['Moderate snow', 0.5, 'snow', '13'] ], // Moderate snow
1222: [imgNames: [day_img: '41.png', night_img: '41.png'], condCode: ['Patchy heavy snow', 0.4, 'snow', '13'] ], // Patchy heavy snow
1225: [imgNames: [day_img: '16.png', night_img: '16.png'], condCode: ['Heavy snow', 0.3, 'snow', '13'] ], // Heavy snow
1237: [imgNames: [day_img: '18.png', night_img: '18.png'], condCode: ['Ice pellets', 0.5, 'sleet', '13'] ], // Ice pellets
1240: [imgNames: [day_img: '11.png', night_img: '11.png'], condCode: ['Light rain shower', 0.8, 'rain', '10'] ], // Light rain shower
1243: [imgNames: [day_img: '12.png', night_img: '12.png'], condCode: ['Moderate or heavy rain shower', 0.3, 'rain', '10'] ], // Moderate or heavy rain shower
1246: [imgNames: [day_img: '12.png', night_img: '12.png'], condCode: ['Torrential rain shower', 0.1, 'rain', '10'] ], // Torrential rain shower
1249: [imgNames: [day_img: '5.png', night_img: '5.png'], condCode: ['Light sleet showers', 0.7, 'sleet', '10'] ], // Light sleet showers
1252: [imgNames: [day_img: '6.png', night_img: '6.png'], condCode: ['Moderate or heavy sleet showers', 0.5, 'sleet', '10'] ], // Moderate or heavy sleet showers
1255: [imgNames: [day_img: '16.png', night_img: '16.png'], condCode: ['Light snow showers', 0.7, 'snow', '13'] ], // Light snow showers
1258: [imgNames: [day_img: '16.png', night_img: '16.png'], condCode: ['Moderate or heavy snow showers', 0.5, 'snow', '13'] ], // Moderate or heavy snow showers
1261: [imgNames: [day_img: '8.png', night_img: '8.png'], condCode: ['Light showers of ice pellets', 0.7, 'sleet', '13'] ], // Light showers of ice pellets
1264: [imgNames: [day_img: '10.png', night_img: '10.png'], condCode: ['Moderate or heavy showers of ice pellets',0.3, 'sleet', '13'] ], // Moderate or heavy showers of ice pellets
1273: [imgNames: [day_img: '38.png', night_img: '47.png'], condCode: ['Patchy light rain with thunder', 0.5, 'tstorms', '11'] ], // Patchy light rain with thunder
1276: [imgNames: [day_img: '35.png', night_img: '35.png'], condCode: ['Moderate or heavy rain with thunder', 0.3, 'tstorms', '11'] ], // Moderate or heavy rain with thunder
1279: [imgNames: [day_img: '41.png', night_img: '46.png'], condCode: ['Patchy light snow with thunder', 0.5, 'tstorms', '11'] ], // Patchy light snow with thunder
1282: [imgNames: [day_img: '18.png', night_img: '18.png'], condCode: ['Moderate or heavy snow with thunder', 0.3, 'tstorms', '11'] ] // Moderate or heavy snow with thunder
]
@Field static attributesMap = [
"betwixt": [title: "Betwixt", descr: "Display the 'slice-of-day'?", typeof: "string", default: "false"],
"cCF": [title: "Cloud cover factor", descr: "", typeof: "number", default: "false"],
"city": [title: "City", descr: "Display your City's name?", typeof: "string", default: "true"],
"cloud": [title: "Cloud", descr: "", typeof: "number", default: "false"],
"condition_code": [title: "Condition code", descr: "", typeof: "number", default: "false"],
"condition_icon_only": [title: "Condition icon only", descr: "", typeof: "string", default: "false"],
"condition_icon_url": [title: "Condition icon URL", descr: "", typeof: "string", default: "false"],
"condition_icon": [title: "Condition icon", descr: "", typeof: "string", default: "false"],
"condition_text": [title: "Condition text", descr: "", typeof: "string", default: "false"],
"country": [title: "Country", descr: "", typeof: "string", default: "false"],
"dashClock": [title: "Clock", descr: "Flash time ':' every 2 seconds?", typeof: "string", default: "false"],
"feelslike_c": [title: "Feels like °C", descr: "Select to display the 'feels like' temperature in C:", typeof: "number", default: "true"],
"feelslike_f": [title: "Feels like °F", descr: "Select to display the 'feels like' temperature in F:", typeof: "number", default: "true"],
"feelslike": [title: "Feels like (in default unit)", descr: "Select to display the 'feels like' temperature:", typeof: "number", default: "true"],
"forecastIcon": [title: "Forecast icon", descr: "Select to display an Icon of the Forecast Weather:", typeof: "string", default: "true"],
"illuminated": [title: "Illuminated", descr: "Illuminance with 'lux' added for use on a Dashboard", typeof: "string", default: "true"],
"is_day": [title: "Is daytime", descr: "", typeof: "number", default: "false"],
"last_updated_epoch": [title: "Last updated epoch", descr: "", typeof: "number", default: "false"],
"last_updated": [title: "Last updated", descr: "", typeof: "string", default: "false"],
"lat": [title: "Latitude and Longitude", descr: "Select to display both Latitude and Longitude", typeof: "number", default: "false"],
"local_date": [title: "Local date", descr: "", typeof: "string", default: "false"],
"localSunrise": [title: "Local Sun Rise and Set", descr: "Select to display the Group of 'Time of Local Sunrise and Sunset,' with and without Dashboard text", typeof: "string", default: "true"],
"local_time": [title: "Local time", descr: "", typeof: "string", default: "false"],
"localtime_epoch": [title: "Localtime epoch", descr: "", typeof: "number", default: "false"],
"location": [title: "Location name with region", descr: "", typeof: "string", default: "false"],
"mytile": [title: "Mytile for dashboard", descr: "", typeof: "string", default: "false"],
"name": [title: "Location name", descr: "Name of Location (duplicates 'City')", typeof: "string", default: "false"],
"open_weather": [title: "OpenWeather attributes", descr: "Select duplicate wind attributes that are specific to Dashboard's Weather template", typeof: false, default: "true"],
"percentPrecip": [title: "Percent precipitation", descr: "Select to display the Chance of Rain, in percent", typeof: "number", default: "true"],
"precipExtended": [title: "Extended Precipitation", descr: "Select to display precipitation over a period of +- 2 days", typeof: false, default: "false"],
"precip_in": [title: "Precipitation Inches", descr: "", typeof: "number", default: "false"],
"precip_mm": [title: "Precipitation MM", descr: "", typeof: "number", default: "false"],
"region": [title: "Region", descr: "", typeof: "string", default: "false"],
"tempHiLow": [title: "Temperature high & low day +1", descr: "Select to display tomorrow's Forecast High and Low Temperatures", typeof: false, default: "true"],
"twilight_begin": [title: "Twilight begin", descr: "", typeof: "string", default: "false"],
"twilight_end": [title: "Twilight end", descr: "", typeof: "string", default: "false"],
"tz_id": [title: "Timezone ID", descr: "", typeof: "string", default: "false"],
"vis_km": [title: "Visibility KM", descr: "", typeof: "number", default: "false"],
"vis_miles": [title: "Visibility miles", descr: "", typeof: "number", default: "false"],
"visual": [title: "Visual weather", descr: "Select to display the Image of the Weather", typeof: "string", default: "true"],
"visualDayPlus1": [title: "Visual weather day +1", descr: "Select to display tomorrow's visual of the Weather", typeof: "string", default: "true"],
"visualDayPlus1WithText": [title: "Visual weather day +1 with text", descr: "", typeof: "string", default: "false"],
"visualWithText": [title: "Visual weather with text", descr: "", typeof: "string", default: "false"],
"weather": [title: "Weather", descr: "Current Conditions", typeof: "string", default: "false"],
"wind_degree": [title: "Wind Degree", descr: "Select to display the Wind Direction (number)", typeof: "number", default: "false"],
"wind_dir": [title: "Wind direction", descr: "Select to display the Wind Direction (letters)", typeof: "string", default: "true"],
"wind_kph": [title: "Wind KPH", descr: "", typeof: "number", default: "false"],
"wind_mph": [title: "Wind MPH", descr: "", typeof: "number", default: "false"],
"wind_mps": [title: "Wind MPS", descr: "Wind in Meters per Second", typeof: "number", default: "false"],
"wind_mytile": [title: "Wind mytile", descr: "", typeof: "string", default: "false"],
"wind": [title: "Wind (in default unit)", descr: "Select to display the Wind Speed", typeof: "number", default: "true"]
]
/*
generic driver stuff
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
*/
/*
installed
Doesn't do much other than call initialize().
*/
def installed()
{
if (descTextEnable) log.info "${state.InternalName ?: device} is Installed"
state.driverInstalled = true
initialize()
}
/*
initialize
called by a reboot and by methods internal to this driver: updated() and installed()
*/
def initialize()
{
unschedule()
migrateTo() // an effort to migrate from pre v1.4.0 to v1.4.0
schedule("11 20 0/8 ? * * *", pollSunRiseSet)
schedule("0 0 8 ? * FRI *", updateCheck)
schedule("11 0/${luxEvery} * * * ?", updateLux)
state.tz_id = TimeZone.getDefault().getID()
if (state?.sunRiseSet?.init == null) state.sunRiseSet = [init:false]
if (state?.apixu?.init == null) state.apixu = [init:false]
if (state?.forecastPrecip?.init == null) {
state.forecastPrecip = [
precipDayMinus2: [inch: 999.9, mm: 999.9],
precipDayMinus1: [inch: 999.9, mm: 999.9],
precipDay0: [inch: 999.9, mm: 999.9],
precipDayPlus1: [inch: 999.9, mm: 999.9],
precipDayPlus2: [inch: 999.9, mm: 999.9],
init: true
]
}
log.trace "${state.InternalName ?: device} was initialized"
runIn(4, updateLux) // give sunrise/set time to complete.
runIn(20, updateCheck) // verify version once
}
private migrateTo() {
if (state?.sunRiseSet?.init) {
pollSunRiseSet()
// state.remove("sunRiseSet") // converted to 'data' storage, no longer need 'state' storage.
state.remove("lowLuxRepeat") // using schedule(), no longer need 'state' storage.
}
}
// Check Version ***** with great thanks and acknowledgment to Cobra (CobraVmax) for his original code ****
def updateCheck()
{
def paramsUD = [uri: "https://hubitatcommunity.github.io/wx-ApiXU/version2.json"]
asynchttpGet("updateCheckHandler", paramsUD)
}
def updateCheckHandler(resp, data) {
state.InternalName = "wx-ApiXU-Driver"
if (resp.getStatus() == 200 || resp.getStatus() == 207) {
respUD = parseJson(resp.data)
//log.warn " Version Checking - Response Data: $respUD" // Troubleshooting Debug Code - Uncommenting this line should show the JSON response from your webserver
state.Copyright = "${thisCopyright}"
// uses reformattted 'version2.json'
def newVerRaw = (respUD.driver.(state.InternalName).ver)
def newVer = (respUD.driver.(state.InternalName).ver.replaceAll("[.vV]", ""))
def currentVer = version().replaceAll("[.vV]", "")
state.UpdateInfo = (respUD.driver.(state.InternalName).updated)
def author = (respUD.author)
// log.debug "updateCheck: $newVerRaw, $state.UpdateInfo, $author"
switch(newVer) {
case { it == "NLS"}:
state.Status = "** This Driver is no longer supported by ${respUD.author} **"
log.warn "** This Driver is no longer supported by ${respUD.author} **"
break
case { it > currentVer}:
state.Status = "New Version Available (Version: ${respUD.driver.(state.InternalName).ver})"
log.warn "** There is a newer version of this Driver available (Version: ${respUD.driver.(state.InternalName).ver}) **"
log.warn "** $state.UpdateInfo **"
break
case { it < currentVer}:
state.Status = "You are using a Test version of this Driver (Expecting: ${respUD.driver.(state.InternalName).ver})"
break
default:
state.Status = "Current"
if (descTextEnable) log.info "You are using the current version of this driver"
break
}
sendEvent(name: "chkUpdate", value: state.UpdateInfo)
sendEvent(name: "chkStatus", value: state.Status)
}
else
{
log.warn "Something went wrong: CHECK THE JSON FILE AND IT'S URI"
}
}
def getThisCopyright(){"© 2019 C Steele "}
//**********************************************************************************************************************
/*
* record of Bangali's version history prior to the Clone:
*
* Version: 5.1.4
* added precipication forecast data from day - 2 to day + 2
* removed selector for duplicate sunrise/sunset (localSunrise == local_sunrise)
*
* Version: 5.1.3
* alternating description for settingEnabled input
*
* Version: 5.1.2
* merged codahq's child device code -- with switch.
*
* Version: 5.1.1
* merged Bangali's v5.0.2 - 5.0.5
*
* Version: 5.1.0
* 4/20/2019: extend attributesMap to contain keyname, title, descr and default
* add debug logging and auto disable
* add settings visibility and auto disable
*
** Version: 5.0.5
* 5/4/2019: fixed typos for feelsLike* and added condition code for day plus 1 forecasted data.
*
* Version: 5.0.2
* 4/20/2019: allow selection for publishing feelsLike and wind attributes
*
* Version: 5.0.1
* 3/24/2019: revert typo
*
* Version: 5.0.0
* 3/10/2019: allow selection of which attributes to publish
* 3/10/2019: restore localSunrise and localSunset attributes
* 3/10/2019: added option for lux polling interval
* 3/10/2019: added expanded weather polling interval
*
* Version: 4.3.1
* 1/20/2019: change icon size for mytile attribute
*
* Version: 4.3.0
* 12/30/2018: removed isStateChange:true based on testing done by @nh.schottfam on hubitat format
*
* Version: 4.2.0
* 12/30/2018: deprecated localSunrise and localSunset attributes instead use local_sunrise and local_sunset respectively
*
* Version: 4.1.0
* 12/29/2018: merged mytile code
*
* Version: 4.0.3
* 12/09/2018: added wind speed in MPS (meters per second)
*
* Version: 4.0.2
* 10/28/2018: continue publishing lux even if apixu api call fails.
*
* Version: 4.0.1
* 10/14/2018: removed logging of weather data.
*
* Version: 4.0.0
* 8/16/2018: added optional weather undergroud mappings.
* 8/16/2018: added forecast icon, high and low temperature for next day.
*
* Version: 3.5.0
* 8/10/2018: added temperature, pressure and humidity capabilities.
*
* Version: 3.0.0
* 7/25/2018: added code contribution from https://github.com/jebbett for new cooler weather icons with icons courtesy
* of https://www.deviantart.com/vclouds/art/VClouds-Weather-Icons-179152045.
*
* Version: 2.5.0
* 5/23/2018: update condition_icon to contain image for use on dashboard and moved icon url to condition_icon_url.
*
* Version: 2.0.0
* 5/29/2018: updated lux calculation with factor from condition code.
*
* Version: 1.0.0
* 5/27/2018: initial release.
*
*/