Yet another weather driver (but for Edge!)

Feb. 2023 Update and Summary

This Edge driver supports 5 weather data sources:

  • WeatherUnderground
  • OpenWeather
  • US Gov
  • Finnish Meteorological Institute
  • WeatherFlow Tempest

Data Provided (depending on availability from source):

  • Current Conditions
    • Temperature/Humidity
    • High/Low Temp
    • Dew Point
    • Precipitation Rate
    • Probability of Precipitation
    • Barometric Pressure
    • Cloud Cover
    • Illuminance
    • UV Index
    • Wind speed, gust, & direction
    • Summary
  • Forecast
    • Temperature/Humidity
    • High/Low Temperature
    • Precipitation Rate
    • Probability of Precipitation
    • Cloud Cover
    • Wind Speed & Gust
    • Summary

Requirements:

  • SmartThings Hub
  • My Edge Bridge Server or some other proxy server to be running on a computer on your LAN. This is required to allow the Edge driver to access URLs outside your network.
  • Account key for access to weather data required for some sources

Key Links:

Installation
Enroll your hub in the channel linked above. Once driver is available on your hub, perform an Add device / Scan for nearby devices in the SmartThings app and a new weather device will be created in your No room assigned room or your hub device room. Go into device settings and configure the options, including the weather data URLs. See the readme file linked above for details.


Original post:

Thereā€™s been some occasional talk on the community about what happens to everyoneā€™s weather SmartApps when Groovy goes away. Until we have some kind of weather solution from SmartThings (after all, the mobile app still has The Weather Channel logo at the bottom?!), Iā€™ve created an Edge weather device driver, but it has a catch. It requires a computer to be always running on your network that can forward weather API requests to the internet. Donā€™t give up just yet!.. Iā€™ve tried to make this part as painless as possible. All it takes is a single executable file to download and run - no configuration and no skills needed beyond knowing how to download a file and run it. (If you donā€™t like the idea of strange programs running on your computer from unknown developers, the source is freely available to inspect!)

The weather device itself is nothing amazing at this point, but it gets the job done by making available the key current weather and forecast data for both display in the mobile app and for use in automation routines.

Of course the weather source itself is everything, and will determine what data is available. Right now this driver supports both Dark Sky (I know itā€™s going away) and US government weather data (free and no account needed). But Iā€™ve structured the driver to be able to easily add additional sources as needed, so am quite open to adding more.

As for that program you need to be running on an always-on computer: If you already have my Edge Bridge Server, nothing else is needed. But Iā€™ve tried to make it even easier for everyone by providing not only a Windows executable but now also a Raspberry Pi executable (Python 3.x not required!). Other Linux or Mac computers with Python 3.x can run the Python script.

Another option is to use your own standard Proxy server, but this is limited to working only for weather API URLs that use HTTP; Iā€™ve found that PROXYing HTTPS requests is not currently possible from Edge drivers, unfortunately. (My Edge Bridge Server works fine with either HTTP or HTTPS API URLs)


If you made it this far, and this project interests you, please reference the README on my Github repository for more information.

Keep in mind the driver is in its initial form and Iā€™m wide open for suggested improvements.

As always, I welcome any and all feedback.


Thanks goes out to @veonua for inspiring me to do this and for providing his Dark Sky URLs for testing!

20 Likes

Very cool. I have it set up.

A couple requests and a question:

  1. Can you include illuminance. The was included on the old Groove weather tile. It can be used to trigger lights as it gets darker or lighter.

  1. The old Groove weather tile let you get data from Personal Weather Stations. It was probably getting the data through Weather Underground. This works good in rural areas that are a long distance from an official weather stations. The following is an example of a personal station.Personal Weather Station Dashboard | Weather Underground

Screenshot_20220731-005149~2

  1. Is there a way to set up multiple weather tiles?
3 Likes

Could English units be provided for precipitation (in./hr.) , Atmospheric Pressure (in. of Hg)?

The old Groovy weather tile provided a move detailed forecast that was hidden in history.

2 Likes

Thanks @TAustin
Can you add another weather provider , I live in Canada !!
I already set up everything to see how it work .

I live in Finland.
Finnish Meteorological Institute provides an Application Programming Interface (API).

API-access (CSW) - Finnish Meteorological Institute (ilmatieteenlaitos.fi)

Is it possible to use weather data from Finnish Meteorological Institute?

3 Likes

I already contacted him about this. But good to see I wouldnā€™t be the only Finn to use it. :smile:

3 Likes

Iā€™ll take a look at it.

2 Likes

I can, please provide the sample URLs for current conditions and forecast.

I could include this, but what would be the source? Iā€™ve not seen it in the weather data Iā€™ve looked at, so would it be a derived value? Perhaps using a combination of cloud cover and UV index??

Iā€™m no expert on these - is there some kind of standard API and data format that they all use? I donā€™t know if Iā€™d want to get into brand/model-specific variations. Iā€™ll have a look at the link you provided to learn more.

I could add a ā€˜create another deviceā€™ button. What would be the purpose of having multiple? Different locations? Or different sources?

This could be done. Ranges for the graphic are fixed, so the only thing you run in to is to find a range that sufficiently fits all unit possibilities without becoming useless. Alternatively, the graphic would be removed and youā€™d only show the numeric value.

For anyone requesting additional weather data sources to be supported, it would be immensely helpful if you could determine the URLs for current conditions and forecast and provide those to me as examples. Pointing me to the API documentation is helpful, but it still requires a bunch of reading and experimentation to figure out exactly what the URLs need to be.

I couldnā€™t provide more detail information about weather data from Finnish Meteorological Institute.

I hope that @Sakari can give to you @TAustin more detail information.

I could include this, but what would be the source? Iā€™ve not seen it in the weather data Iā€™ve looked at, so would it be a derived value? Perhaps using a combination of cloud cover and UV index??

Standard US weather stations do not report lux because there are too many local variables. They do report visibility, which is considered more standardized.

About ASOS

Over the years, some people have used cloud cover as a proxy, but that wonā€™t always pick up fog. Or pollution, including wildfire smoke. Or a solar eclipse. :thinking:

Some individual commercial weather stations have a lux reading, but most donā€™t. Instead, most people use a specific sensor in the exact spot they want to measure from.

The HuĆ© outdoor motion sensor is a popular outdoor light sensor that works with smartthings, for what thatā€™s worth. @iquix has an Edge Driver that exposes the illumination reading:

[Edge Driver] Hue Motion Sensor (No Hue Bridge)

2 Likes

hi will this work in the uk . thanks

Youā€™ll need a weather source that you are happy with. If you already have a Dark Sky account, then that could work. Iā€™m not sure about others that carry data for your country. Hopefully others here can suggest a source.

FYI, Iā€™m making progress on the Finnish Meteorological Institute, so hopefully that will be added soon.

2 Likes

Illuminance - the old Groove weather tile had illuminance. Looking through old posts it appears the the Groovy weather tile used Weather Underground as it source, so I assumed that was the source. But looking at Weather Underground today I donā€™t see illuminance. So I donā€™t know where the data comes from.

Personal Weather Stations (PWS) - the old Groove weather tile had access to Personal Weather Station data. Looking through old posts it appears the the Groovy weather tile used Weather Underground as it source. Today I can access PWS data through Weather Underground. The old Groovy weather tiles are still functional and contain this data, but I expect them to die in the near future.

Multiple weather tiles - I have 2 ST locations. The 1st has a hub and 2nd doesnā€™t. I could install a 2nd weather tile on the hub location and use SharpTools to use the data in automations for the 2nd location. The 2nd location is rural and is where I use the PWS data.

The metric measurements for rainfall and pressure mean nothing to me. English measurements with no graphics would mean more to me.

1 Like

I did some further research. The SmartThings SmartWeather Station Tile data source was switched from the WeatherUnderground (WU) to The Weather Company (TWC) in Jan of 2019.

2 Likes

The DTH for the SmartWeather Station Tile is shown below:

Twice it says estimateLux so you may be right that it is a calulcated value:

send(name: ā€œilluminanceā€, value: estimateLux(obs, sunriseDate, sunsetDate)) &
send(name: ā€œilluminanceā€, value: estimateLux(cond, sunriseDate, sunsetDate))

/**
 *  Copyright 2015 SmartThings
 *
 *  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.
 *
 *  SmartWeather Station
 *
 *  Author: SmartThings
 *
 *  Date: 2013-04-30
 */
metadata {
    definition (name: "SmartWeather Station Tile", namespace: "smartthings", author: "SmartThings") {
        capability "Illuminance Measurement"
        capability "Temperature Measurement"
        capability "Relative Humidity Measurement"
        capability "Ultraviolet Index"
        capability "Wind Speed"
        capability "stsmartweather.windSpeed"
        capability "stsmartweather.windDirection"
        capability "stsmartweather.apparentTemperature"
        capability "stsmartweather.astronomicalData"
        capability "stsmartweather.precipitation"
        capability "stsmartweather.ultravioletDescription"
        capability "stsmartweather.weatherAlert"
        capability "stsmartweather.weatherForecast"
        capability "stsmartweather.weatherSummary"
        capability "Sensor"
        capability "Refresh"
    }

    preferences {
        input "zipCode", "text", title: "Zip Code (optional)", required: false
        input "stationId", "text", title: "Personal Weather Station ID (optional)", required: false
    }

    tiles(scale: 2) {
        valueTile("temperature", "device.temperature", height: 2, width: 2) {
            state "default", label:'${currentValue}Ā°',
                    backgroundColors:[
                            [value: 31, color: "#153591"],
                            [value: 44, color: "#1e9cbb"],
                            [value: 59, color: "#90d2a7"],
                            [value: 74, color: "#44b621"],
                            [value: 84, color: "#f1d801"],
                            [value: 95, color: "#d04e00"],
                            [value: 96, color: "#bc2323"]
                    ]
        }

        valueTile("feelsLike", "device.feelsLike", decoration: "flat", height: 1, width: 2) {
            state "default", label:'Feels like ${currentValue}Ā°'
        }

        standardTile("weatherIcon", "device.weatherIcon", decoration: "flat", height: 2, width: 2) {
            state "0", icon:"https://smartthings-twc-icons.s3.amazonaws.com/00.png", label: ""
            state "1", icon:"https://smartthings-twc-icons.s3.amazonaws.com/01.png", label: ""
            state "2", icon:"https://smartthings-twc-icons.s3.amazonaws.com/02.png", label: ""
            state "3", icon:"https://smartthings-twc-icons.s3.amazonaws.com/03.png", label: ""
            state "4", icon:"https://smartthings-twc-icons.s3.amazonaws.com/04.png", label: ""
            state "5", icon:"https://smartthings-twc-icons.s3.amazonaws.com/05.png", label: ""
            state "6", icon:"https://smartthings-twc-icons.s3.amazonaws.com/06.png", label: ""
            state "7", icon:"https://smartthings-twc-icons.s3.amazonaws.com/07.png", label: ""
            state "8", icon:"https://smartthings-twc-icons.s3.amazonaws.com/08.png", label: ""
            state "9", icon:"https://smartthings-twc-icons.s3.amazonaws.com/09.png", label: ""
            state "10", icon:"https://smartthings-twc-icons.s3.amazonaws.com/10.png", label: ""
            state "11", icon:"https://smartthings-twc-icons.s3.amazonaws.com/11.png", label: ""
            state "12", icon:"https://smartthings-twc-icons.s3.amazonaws.com/12.png", label: ""
            state "13", icon:"https://smartthings-twc-icons.s3.amazonaws.com/13.png", label: ""
            state "14", icon:"https://smartthings-twc-icons.s3.amazonaws.com/14.png", label: ""
            state "15", icon:"https://smartthings-twc-icons.s3.amazonaws.com/15.png", label: ""
            state "16", icon:"https://smartthings-twc-icons.s3.amazonaws.com/16.png", label: ""
            state "17", icon:"https://smartthings-twc-icons.s3.amazonaws.com/17.png", label: ""
            state "18", icon:"https://smartthings-twc-icons.s3.amazonaws.com/18.png", label: ""
            state "19", icon:"https://smartthings-twc-icons.s3.amazonaws.com/19.png", label: ""
            state "20", icon:"https://smartthings-twc-icons.s3.amazonaws.com/20.png", label: ""
            state "21", icon:"https://smartthings-twc-icons.s3.amazonaws.com/21.png", label: ""
            state "22", icon:"https://smartthings-twc-icons.s3.amazonaws.com/22.png", label: ""
            state "23", icon:"https://smartthings-twc-icons.s3.amazonaws.com/23.png", label: ""
            state "24", icon:"https://smartthings-twc-icons.s3.amazonaws.com/24.png", label: ""
            state "25", icon:"https://smartthings-twc-icons.s3.amazonaws.com/25.png", label: ""
            state "26", icon:"https://smartthings-twc-icons.s3.amazonaws.com/26.png", label: ""
            state "27", icon:"https://smartthings-twc-icons.s3.amazonaws.com/27.png", label: ""
            state "28", icon:"https://smartthings-twc-icons.s3.amazonaws.com/28.png", label: ""
            state "29", icon:"https://smartthings-twc-icons.s3.amazonaws.com/29.png", label: ""
            state "30", icon:"https://smartthings-twc-icons.s3.amazonaws.com/30.png", label: ""
            state "31", icon:"https://smartthings-twc-icons.s3.amazonaws.com/31.png", label: ""
            state "32", icon:"https://smartthings-twc-icons.s3.amazonaws.com/32.png", label: ""
            state "33", icon:"https://smartthings-twc-icons.s3.amazonaws.com/33.png", label: ""
            state "34", icon:"https://smartthings-twc-icons.s3.amazonaws.com/34.png", label: ""
            state "35", icon:"https://smartthings-twc-icons.s3.amazonaws.com/35.png", label: ""
            state "36", icon:"https://smartthings-twc-icons.s3.amazonaws.com/36.png", label: ""
            state "37", icon:"https://smartthings-twc-icons.s3.amazonaws.com/37.png", label: ""
            state "38", icon:"https://smartthings-twc-icons.s3.amazonaws.com/38.png", label: ""
            state "39", icon:"https://smartthings-twc-icons.s3.amazonaws.com/39.png", label: ""
            state "40", icon:"https://smartthings-twc-icons.s3.amazonaws.com/40.png", label: ""
            state "41", icon:"https://smartthings-twc-icons.s3.amazonaws.com/41.png", label: ""
            state "42", icon:"https://smartthings-twc-icons.s3.amazonaws.com/42.png", label: ""
            state "43", icon:"https://smartthings-twc-icons.s3.amazonaws.com/43.png", label: ""
            state "44", icon:"https://smartthings-twc-icons.s3.amazonaws.com/44.png", label: ""
            state "45", icon:"https://smartthings-twc-icons.s3.amazonaws.com/45.png", label: ""
            state "46", icon:"https://smartthings-twc-icons.s3.amazonaws.com/46.png", label: ""
            state "47", icon:"https://smartthings-twc-icons.s3.amazonaws.com/47.png", label: ""
            state "na", icon:"https://smartthings-twc-icons.s3.amazonaws.com/na.png", label: ""
        }

        valueTile("humidity", "device.humidity", decoration: "flat", height: 1, width: 2) {
            state "default", label:'${currentValue}% humidity'
        }

        valueTile("wind", "device.windVector", decoration: "flat", height: 1, width: 2) {
            state "default", label:'Wind\n${currentValue}'
        }

        valueTile("weather", "device.weather", decoration: "flat", height: 1, width: 2) {
            state "default", label:'${currentValue}'
        }

        valueTile("city", "device.city", decoration: "flat", height: 1, width: 2) {
            state "default", label:'${currentValue}'
        }

        valueTile("percentPrecip", "device.percentPrecip", decoration: "flat", height: 1, width: 2) {
            state "default", label:'${currentValue}% precip'
        }

        valueTile("ultravioletIndex", "device.uvDescription", decoration: "flat", height: 1, width: 2) {
            state "default", label:'UV ${currentValue}'
        }

        valueTile("alert", "device.alert", decoration: "flat", height: 2, width: 6) {
            state "default", label:'${currentValue}'
        }

        standardTile("refresh", "device.refresh", decoration: "flat", height: 1, width: 2) {
            state "default", label: "", action: "refresh", icon:"st.secondary.refresh"
        }

        valueTile("rise", "device.localSunrise", decoration: "flat", height: 1, width: 2) {
            state "default", label:'Sunrise ${currentValue}'
        }

        valueTile("set", "device.localSunset", decoration: "flat", height: 1, width: 2) {
            state "default", label:'Sunset ${currentValue}'
        }

        valueTile("light", "device.illuminance", decoration: "flat", height: 1, width: 2) {
            state "default", label:'${currentValue} lux'
        }

        valueTile("today", "device.forecastToday", decoration: "flat", height: 1, width: 3) {
            state "default", label:'Today:\n${currentValue}'
        }

        valueTile("tonight", "device.forecastTonight", decoration: "flat", height: 1, width: 3) {
            state "default", label:'Tonight:\n${currentValue}'
        }

        valueTile("tomorrow", "device.forecastTomorrow", decoration: "flat", height: 1, width: 3) {
            state "default", label:'Tomorrow:\n${currentValue}'
        }

        valueTile("lastUpdate", "device.lastUpdate", decoration: "flat", height: 1, width: 3) {
            state "default", label:'Last update:\n${currentValue}'
        }

        main(["temperature", "weatherIcon","feelsLike"])
        details(["temperature", "feelsLike", "weatherIcon", "humidity", "wind",
                 "weather", "city", "percentPrecip", "ultravioletIndex", "light",
                 "rise", "set",
                 "refresh",
                 "today", "tonight", "tomorrow", "lastUpdate",
                 "alert"])}
}

// parse events into attributes
def parse(String description) {
    log.debug "Parsing '${description}'"
}

def installed() {
    schedulePoll()
    poll()
}

def schedulePoll() {
    unschedule()
    runEvery3Hours("poll")
}

def updated() {
    schedulePoll()
    poll()
}

def uninstalled() {
    unschedule()
}

// handle commands
def poll() {
    log.debug "WUSTATION: Executing 'poll', location: ${location.name}"
    if (stationId) {
        pollUsingPwsId(stationId.toUpperCase())
    } else {
        if (zipCode && zipCode.toUpperCase().startsWith('PWS:')) {
            log.debug zipCode.substring(4)
            pollUsingPwsId(zipCode.substring(4).toUpperCase())
        } else {
            pollUsingZipCode(zipCode?.toUpperCase())
        }
    }
}

def pollUsingZipCode(String zipCode) {
    // Last update time stamp
    def timeZone = location.timeZone ?: timeZone(timeOfDay)
    def timeStamp = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
    send(name: "lastUpdate", value: timeStamp)

    // Current conditions
    def tempUnits = getTemperatureScale()
    def windUnits = tempUnits == "C" ? "KPH" : "MPH"
    def obs = getTwcConditions(zipCode)
    if (obs) {
        // TODO def weatherIcon = obs.icon_url.split("/")[-1].split("\\.")[0]

        send(name: "temperature", value: obs.temperature, unit: tempUnits)
        send(name: "feelsLike", value: obs.temperatureFeelsLike, unit: tempUnits)

        send(name: "humidity", value: obs.relativeHumidity, unit: "%")
        send(name: "weather", value: obs.wxPhraseLong)
        send(name: "weatherIcon", value: obs.iconCode, displayed: false)

        send(name: "wind", value: obs.windSpeed, unit: windUnits)
        send(name: "windspeed", value: new BigDecimal(convertWindSpeed(obs.windSpeed, tempUnits == "F" ? "imperial" : "metric", "metric") / 3.6).setScale(2, BigDecimal.ROUND_HALF_UP), unit: "m/s")
        send(name: "windVector", value: "${obs.windDirectionCardinal} ${obs.windSpeed} ${windUnits}")

        log.trace "Getting location info"
        def loc = getTwcLocation(zipCode)?.location
        def cityValue = createCityName(loc) ?: zipCode // I don't think we'll ever hit a point where we can't build a city name... But just in case...
        if (cityValue != device.currentValue("city")) {
            send(name: "city", value: cityValue, isStateChange: true)
        }

        send(name: "ultravioletIndex", value: obs.uvIndex)
        send(name: "uvDescription", value: obs.uvDescription)

        def dtf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")

        def sunriseDate = dtf.parse(obs.sunriseTimeLocal)
        log.debug "'${obs.sunriseTimeLocal}'"

        def sunsetDate = dtf.parse(obs.sunsetTimeLocal)

        def tf = new java.text.SimpleDateFormat("h:mm a")
        tf.setTimeZone(TimeZone.getTimeZone(loc?.ianaTimeZone))

        def localSunrise = "${tf.format(sunriseDate)}"
        def localSunset = "${tf.format(sunsetDate)}"
        send(name: "localSunrise", value: localSunrise, descriptionText: "Sunrise today is at $localSunrise")
        send(name: "localSunset", value: localSunset, descriptionText: "Sunset today at is $localSunset")

        send(name: "illuminance", value: estimateLux(obs, sunriseDate, sunsetDate))

        // Forecast
        def f = getTwcForecast(zipCode)
        if (f) {
            def icon = f.daypart[0].iconCode[0] != null ? f.daypart[0].iconCode[0] : f.daypart[0].iconCode[1]
            def precip = f.daypart[0].precipChance[0] != null ? f.daypart[0].precipChance[0] : f.daypart[0].precipChance[1]
            def narrative = f.daypart[0].narrative

            send(name: "percentPrecip", value: precip, unit: "%")
            send(name: "forecastIcon", value: icon, displayed: false)
            send(name: "forecastToday", value: narrative[0] ?: "n/a")
            send(name: "forecastTonight", value: narrative[1] ?: "n/a")
            send(name: "forecastTomorrow", value: narrative[2] ?: "n/a")
        } else {
            log.warn "Forecast not found"
            send(name: "percentPrecip", value: 0, unit: "%", descriptionText: "Chance of precipitation could not be found")
            send(name: "forecastIcon", value: "", displayed: false)
            send(name: "forecastToday", value: "n/a", descriptionText: "Today's forecast could not be found")
            send(name: "forecastTonight", value: "n/a", descriptionText: "Tonight's forecast could not be found")
            send(name: "forecastTomorrow", value: "n/a", descriptionText: "Tomorrow's forecast could not be found")
        }

        // Alerts
        def alerts = getTwcAlerts("${loc?.latitude},${loc?.longitude}")
        if (alerts) {
            alerts.each {alert ->
                def msg = alert.headlineText
                if (alert.effectiveTimeLocal && !msg.contains(" from ")) {
                    msg += " from ${parseAlertTime(alert.effectiveTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.effectiveTimeLocalTimeZone))}"
                }
                if (alert.expireTimeLocal && !msg.contains(" until ")) {
                    msg += " until ${parseAlertTime(alert.expireTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.expireTimeLocalTimeZone))}"
                }
                send(name: "alert", value: msg, descriptionText: msg)
            }
        } else {
            send(name: "alert", value: "No current alerts", descriptionText: msg)
        }
    } else {
        log.warn "No response from TWC API"
    }

    return null
}

def pollUsingPwsId(String stationId) {
    // Last update time stamp
    def timeZone = location.timeZone ?: timeZone(timeOfDay)
    def timeStamp = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone)
    sendEvent(name: "lastUpdate", value: timeStamp)

    // Current conditions
    def tempUnits = getTemperatureScale()
    def windUnits = tempUnits == "C" ? "KPH" : "MPH"
    def obsWrapper = getTwcPwsConditions(stationId)
    if (obsWrapper && obsWrapper.observations && obsWrapper.observations.size()) {
        def obs = obsWrapper.observations[0]
        def dataScale = obs.imperial ? 'imperial' : 'metric'

        send(name: "temperature", value: convertTemperature(obs[dataScale].temp, dataScale, tempUnits), unit: tempUnits)
        send(name: "feelsLike", value: convertTemperature(obs[dataScale].windChill, dataScale, tempUnits), unit: tempUnits)

        send(name: "humidity", value: obs.humidity, unit: "%")

        def windSpeed = convertWindSpeed(obs[dataScale].windSpeed, dataScale, tempUnits)
        send(name: "wind", value: windSpeed, unit: windUnits)
        send(name: "windspeed", value: new BigDecimal(convertWindSpeed(obs[dataScale].windSpeed, dataScale, "metric") / 3.6).setScale(2, BigDecimal.ROUND_HALF_UP), unit: "m/s")
        send(name: "windVector", value: "${obs.winddir}Ā° ${windSpeed} ${windUnits}")

        def loc = getTwcLocation("${obs.lat},${obs.lon}")?.location
        def cityValue = createCityName(loc) ?: "${obs.neighborhood}, ${obs.country}"
        if (cityValue != device.currentValue("city")) {
            send(name: "city", value: cityValue, isStateChange: true)
        }

        send(name: "ultravioletIndex", value: obs.uv)

        def cond = getTwcConditions("${obs.lat},${obs.lon}")
        if (cond) {
            send(name: "weather", value: cond.wxPhraseLong)
            send(name: "weatherIcon", value: cond.iconCode, displayed: false)
            send(name: "uvDescription", value: cond.uvDescription)

            def dtf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
            def sunriseDate = dtf.parse(cond.sunriseTimeLocal)
            log.debug "'${cond.sunriseTimeLocal}'"

            def sunsetDate = dtf.parse(cond.sunsetTimeLocal)
            def tf = new java.text.SimpleDateFormat("h:mm a")
            tf.setTimeZone(TimeZone.getTimeZone(loc?.ianaTimeZone))

            def localSunrise = "${tf.format(sunriseDate)}"
            def localSunset = "${tf.format(sunsetDate)}"
            send(name: "localSunrise", value: localSunrise, descriptionText: "Sunrise today is at $localSunrise")
            send(name: "localSunset", value: localSunset, descriptionText: "Sunset today at is $localSunset")

            send(name: "illuminance", value: estimateLux(cond, sunriseDate, sunsetDate))
        } else {
            log.warn "Conditions not found"
            send(name: "weather", value: "n/a", descriptionText: "Weather summary could not be found")
            send(name: "weatherIcon", value: "", displayed: false)
            send(name: "uvDescription", value: "n/a")

            send(name: "localSunrise", value: "n/a", descriptionText: "Sunrise time could not be found")
            send(name: "localSunset", value: "n/a", descriptionText: "Sunset time could not be found")
            send(name: "illuminance", value: 0, descriptionText: "Illuminance could not be found")
        }

        // Forecast
        def f = getTwcForecast("${obs.lat},${obs.lon}")
        if (f) {
            def icon = f.daypart[0].iconCode[0] != null ? f.daypart[0].iconCode[0] : f.daypart[0].iconCode[1]
            def precip = f.daypart[0].precipChance[0] != null ? f.daypart[0].precipChance[0] : f.daypart[0].precipChance[1]
            def narrative = f.daypart[0].narrative

            send(name: "percentPrecip", value: precip, unit: "%")
            send(name: "forecastIcon", value: icon, displayed: false)
            send(name: "forecastToday", value: narrative[0] ?: "n/a")
            send(name: "forecastTonight", value: narrative[1] ?: "n/a")
            send(name: "forecastTomorrow", value: narrative[2] ?: "n/a")
        } else {
            log.warn "Forecast not found"
            send(name: "percentPrecip", value: 0, unit: "%", descriptionText: "Chance of precipitation could not be found")
            send(name: "forecastIcon", value: "", displayed: false)
            send(name: "forecastToday", value: "n/a", descriptionText: "Today's forecast could not be found")
            send(name: "forecastTonight", value: "n/a", descriptionText: "Tonight's forecast could not be found")
            send(name: "forecastTomorrow", value: "n/a", descriptionText: "Tomorrow's forecast could not be found")
        }

        // Alerts
        def alerts = getTwcAlerts("${obs.lat},${obs.lon}")
        if (alerts) {
            alerts.each {alert ->
                def msg = alert.headlineText
                if (alert.effectiveTimeLocal && !msg.contains(" from ")) {
                    msg += " from ${parseAlertTime(alert.effectiveTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.effectiveTimeLocalTimeZone))}"
                }
                if (alert.expireTimeLocal && !msg.contains(" until ")) {
                    msg += " until ${parseAlertTime(alert.expireTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.expireTimeLocalTimeZone))}"
                }
                send(name: "alert", value: msg, descriptionText: msg)
            }
        } else {
            send(name: "alert", value: "No current alerts", descriptionText: msg)
        }
    } else {
        log.warn "No response from TWC API"
    }

    return null
}

def parseAlertTime(s) {
    def dtf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
    def s2 = s.replaceAll(/([0-9][0-9]):([0-9][0-9])$/,'$1$2')
    dtf.parse(s2)
}

def refresh() {
    poll()
}

def configure() {
    poll()
}

private pad(String s, size = 25) {
    def n = (size - s.size()) / 2
    if (n > 0) {
        def sb = ""
        n.times {sb += " "}
        sb += s
        n.times {sb += " "}
        return sb
    }
    else {
        return s
    }
}


private get(feature) {
    getWeatherFeature(feature, zipCode)
}

private localDate(timeZone) {
    def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
    df.setTimeZone(TimeZone.getTimeZone(timeZone))
    df.format(new Date())
}

private send(Map map) {
    //log.trace "WUSTATION: event: $map"
    sendEvent(map)
}

private estimateLux(obs, sunriseDate, sunsetDate) {
    def lux = 0
    if (obs.dayOrNight == 'N') {
        lux = 10
    } else {
        //day
        switch(obs.iconCode) {
            case 4:
                lux = 200
                break
            case 5..26:
                lux = 1000
                break
            case 27..28:
                lux = 2500
                break
            case 29..30:
                lux = 7500
                break
            default:
                //sunny, clear
                lux = 10000
        }

        //adjust for dusk/dawn
        def now = new Date().time
        def afterSunrise = now - sunriseDate.time
        def beforeSunset = sunsetDate.time - now
        def oneHour = 1000 * 60 * 60

        if (afterSunrise < oneHour) {
            //dawn
            lux = (long)(lux * (afterSunrise/oneHour))
        } else if (beforeSunset < oneHour) {
            //dusk
            lux = (long)(lux * (beforeSunset/oneHour))
        }
    }
    lux
}

private fixScale(scale) {
    switch (scale.toLowerCase()) {
        case "c":
        case "metric":
            return "metric"
        default:
            return "imperial"
    }
}

private convertTemperature(value, fromScale, toScale) {
    def fs = fixScale(fromScale)
    def ts = fixScale(toScale)
    if (fs == ts) {
        return value
    }
    if (ts == 'imperial') {
        return value * 9.0 / 5.0 + 32.0
    }
    return (value - 32.0) * 5.0 / 9.0
}

private convertWindSpeed(value, fromScale, toScale) {
    def fs = fixScale(fromScale)
    def ts = fixScale(toScale)
    if (fs == ts) {
        return value
    }
    if (ts == 'imperial') {
        return value / 1.609
    }
    return value * 1.609
}

private createCityName(location) {
    def cityName = null

    if (location) {
        cityName = location.city + ", "

        if (location.adminDistrictCode) {
            cityName += location.adminDistrictCode
            cityName += " "
            cityName += location.countryCode ?: location.country
        } else {
            cityName += location.country
        }
    }

    cityName
}

hi just installed the driver and did add device but found nothing .
? Select a driver. 1
2022-08-01T16:09:29.813482489+00:00 TRACE Edge Weather V1 Setup driver thisDriver with lifecycle handlers:
DeviceLifecycleDispatcher: thisDriver
default_handlers:
added:
init:
doConfigure:
infoChanged:
removed:
driverSwitched:
child_dispatchers:

2022-08-01T16:09:29.820737740+00:00 TRACE Edge Weather V1 Setup driver thisDriver with Capability handlers:
CapabilityCommandDispatcher: thisDriver
default_handlers:
partyvoice23922.refresh:
push
child_dispatchers:

2022-08-01T16:09:29.832229274+00:00 INFO Edge Weather V1 Weather Driver v0.1 Started
2022-08-01T16:09:29.853300388+00:00 TRACE Edge Weather V1 Received event with handler environment_info
2022-08-01T16:09:29.859965594+00:00 TRACE Edge Weather V1 Received event with handler environment_info
2022-08-01T16:09:29.868537502+00:00 DEBUG Edge Weather V1 Z-Wave hub node ID environment changed.
2022-08-01T16:09:29.878485140+00:00 TRACE Edge Weather V1 Received event with handler discovery
2022-08-01T16:09:29.894273364+00:00 INFO Edge Weather V1 Creating Web Request device
2022-08-01T16:09:29.907081640+00:00 DEBUG Edge Weather V1 Exiting device creation
2022-08-01T16:09:29.912386124+00:00 DEBUG Edge Weather V1 discovery device thread event handled
2022-08-01T16:10:59.050795024+00:00 TRACE Edge Weather V1 Received event with handler discovery
2022-08-01T16:10:59.107233521+00:00 TRACE Edge Weather V1 Received event with handler driver_lifecycle
2022-08-01T16:10:59.131854825+00:00 INFO Edge Weather V1 *** Driver being shut down ***
2022-08-01T16:10:59.151213125+00:00 DEBUG Edge Weather V1 driver device thread event handled
2022-08-01T16:11:19.070504629+00:00 TRACE Edge Weather V1 Setup driver thisDriver with lifecycle handlers:
DeviceLifecycleDispatcher: thisDriver
default_handlers:
init:
doConfigure:
added:
driverSwitched:
removed:
infoChanged:
child_dispatchers:

2022-08-01T16:11:19.073772595+00:00 TRACE Edge Weather V1 Setup driver thisDriver with Capability handlers:
CapabilityCommandDispatcher: thisDriver
default_handlers:
partyvoice23922.refresh:
push
child_dispatchers:

2022-08-01T16:11:19.077096090+00:00 INFO Edge Weather V1 Weather Driver v0.1 Started
2022-08-01T16:11:19.087364596+00:00 TRACE Edge Weather V1 Received event with handler environment_info
2022-08-01T16:11:19.091156783+00:00 TRACE Edge Weather V1 Received event with handler environment_info
2022-08-01T16:11:19.094119889+00:00 DEBUG Edge Weather V1 Z-Wave hub node ID environment changed.
2022-08-01T16:11:19.098520713+00:00 TRACE Edge Weather V1 Received event with handler discovery
2022-08-01T16:11:19.104664742+00:00 INFO Edge Weather V1 Creating Web Request device
2022-08-01T16:11:19.112366591+00:00 DEBUG Edge Weather V1 Exiting device creation
2022-08-01T16:11:19.115126845+00:00 DEBUG Edge Weather V1 discovery device thread event handled
2022-08-01T16:11:58.036092941+00:00 TRACE Edge Weather V1 Received event with handler discovery
2022-08-01T16:11:58.294579286+00:00 TRACE Edge Weather V1 Received event with handler driver_lifecycle
2022-08-01T16:11:58.411310790+00:00 INFO Edge Weather V1 *** Driver being shut down ***
2022-08-01T16:11:58.426954024+00:00 DEBUG Edge Weather V1 driver device thread event handled
2022-08-01T16:12:19.001342580+00:00 TRACE Edge Weather V1 Setup driver thisDriver with lifecycle handlers:
DeviceLifecycleDispatcher: thisDriver
default_handlers:
doConfigure:
infoChanged:
driverSwitched:
added:
removed:
init:
child_dispatchers:

2022-08-01T16:12:19.009975102+00:00 TRACE Edge Weather V1 Setup driver thisDriver with Capability handlers:
CapabilityCommandDispatcher: thisDriver
default_handlers:
partyvoice23922.refresh:
push
child_dispatchers:

2022-08-01T16:12:19.018719764+00:00 INFO Edge Weather V1 Weather Driver v0.1 Started
2022-08-01T16:12:19.045851907+00:00 TRACE Edge Weather V1 Received event with handler environment_info
2022-08-01T16:12:19.052745894+00:00 TRACE Edge Weather V1 Received event with handler environment_info
2022-08-01T16:12:19.060885378+00:00 DEBUG Edge Weather V1 Z-Wave hub node ID environment changed.
2022-08-01T16:12:19.070914974+00:00 TRACE Edge Weather V1 Received event with handler discovery
2022-08-01T16:12:19.084152881+00:00 INFO Edge Weather V1 Creating Web Request device
2022-08-01T16:12:19.097242047+00:00 DEBUG Edge Weather V1 Exiting device creation
2022-08-01T16:12:19.102672678+00:00 DEBUG Edge Weather V1 discovery device thread event handled
listening for logsā€¦ |

Update: As previously announced, the Dark Sky iOS app will no longer be available beginning on December 31st, 2022 and, as of this date, already purchased versions of the app will no longer provide weather data. The Dark Sky API and website will continue to function until March 31st, 2023.

Dark Skyā€™s forecast technology is now enhanced and integrated into the all-new Apple Weather forecast, powering Appleā€™s updated Weather app. Click here to learn more.

Developers, Appleā€™s new WeatherKit API lets you incorporate Apple Weather forecast data into your app and is available for iOS, iPadOS, macOS, tvOS, and web. Learn more here.