Weather Underground

I don't know about anyone else, but I have always used the Weather Channel for online weather, probably since I grew up with the cable TV Weather Channel. It was just always my go to.

They have made so many changes to the Weather Channel site and app in the last year, I find it almost unusable now. You even need to make an account to see a full day of hourly forecasts now.

Since Weather Underground is part of the Weather Channel, it gets the same exact forecasts. The WU Website allows for many days of hourly forecasts, which is a premium paid feature on the The Weather Channel. I find the wunderground.com to be a much superior site compared to the Weather Channel these days, even if it is not quite as flashy.

The WU phone app also isn't bombarded with Ads like the Weather Channel app, but it is still all the same forecast info. It is nice how current conditions are based on PWS stations around me, including my own Ecowitt.

My big realization this week was finding out that I have free access to the Underground Weather APIs because my Ecowitt weather station is a PWS contributor. I have been uploading my PWS data for years now, and I never new it basically gives me free access to the Weather Channel forecast API endpoints to use in Hubitat.

I had AI make me a driver, and I am using that for my rain predictions for my gardens irrigation app rain delay and cancels now. Beyond that I am now using the WU phone app and website as well, so I am now all-in on WU.

Did you try this driver yet from HPM?

I saw that, but it was overkill for what I needed. I really just needed rain percent and rain inches for today and the next three days, to use with my irrigation app. So I had AI make a driver for that, but it does also pull in most current conditions as well.

I can paste it back into AI and have it add any additional attributes if needed.

/**
 * Weather Underground PWS & Forecast Driver for Hubitat Elevation
 * Licensed under the Apache License, Version 2.0
 */
metadata {
    definition (
        name: "Weather Underground PWS with Forecast", 
        namespace: "custom", 
        author: "AI Collaborator"
    ) {
        capability "Sensor"
        capability "Refresh"
        capability "TemperatureMeasurement"
        capability "RelativeHumidityMeasurement"
        capability "PressureMeasurement"
        
        attribute "weather", "string"
        attribute "windSpeed", "number"
        attribute "windDirection", "number"
        attribute "windDirectionStr", "string"
        attribute "windGust", "number"
        attribute "solarRadiation", "number"
        attribute "uvIndex", "number"
        attribute "precipRate", "number"
        attribute "precipTotal", "number"
        attribute "dewPoint", "number"
        attribute "heatIndex", "number"
        attribute "windChill", "number"
        attribute "forecastTodayMax", "number"
        attribute "forecastTodayMin", "number"
        attribute "forecastTodayNarrative", "string"
        attribute "forecastTomorrowMax", "number"
        attribute "forecastTomorrowMin", "number"
        attribute "forecastTomorrowNarrative", "string"
        attribute "precipChanceToday", "number"
        attribute "precipChanceTomorrow", "number"
        attribute "precipChanceDay2", "number"
        attribute "precipChanceDay3", "number"
        attribute "rainExpectedToday", "number"
        attribute "rainExpectedTomorrow", "number"
        attribute "rainExpectedDay2", "number"
        attribute "rainExpectedDay3", "number"
        attribute "lastUpdateTime", "string"
    }
}

preferences {
    input name: "apiKey", type: "text", title: "Weather Underground API Key", required: true
    input name: "stationId", type: "text", title: "PWS Station ID", required: true
    input name: "unitSystem", type: "enum", title: "Unit System", 
          options: ["e", "m"], defaultValue: "e", required: true
    input name: "refreshInterval", type: "enum", title: "Poll Interval (Minutes)", 
          options: ["5", "10", "15", "30", "60"], defaultValue: "15", required: true
          
    // CHANGED TO BOOL: Renders as a true/false toggle slider component natively
    input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: true
}

def installed() { 
    log.info "WU PWS Driver Installed"
    initialize() 
}

def updated() { 
    log.info "WU PWS Driver Updated"
    unschedule()
    initialize() 
}

def initialize() {
    def cronMin = settings.refreshInterval ?: "15"
    schedule("0 */" + cronMin + " * ? * *", refresh)
    refresh()
}

def refresh() {
    if (logEnable) log.debug "Weather Underground refresh initiated"
    if (!settings.apiKey || !settings.stationId) { 
        log.warn "Missing configuration attributes"
        return 
    }
    def units = settings.unitSystem ?: "e"
    def qMap = [stationId: settings.stationId, format: "json", units: units, apiKey: settings.apiKey]
    
    // RESTORED: Fixed back to api.weather.com exactly as your working copy required
    asynchttpGet("parseCurrentResponse", [uri: "https://api.weather.com", path: "/v2/pws/observations/current", query: qMap, timeout: 10])
}
def parseCurrentResponse(response, data) {
    if (response.hasError() || response.status != 200) { 
        log.error "Current observations download failed with status code: ${response.status}"
        return 
    }
    try {
        def json = parseJson(response.data)
        if (logEnable) log.debug "Current Obs Response JSON correctly parsed"
        def obs = json.observations.get(0)
        def units = settings.unitSystem ?: "e"
        def u = (units == "e") ? obs.imperial : obs.metric
        
        device.sendEvent(name: "temperature", value: u.temp, unit: (units == "e" ? "°F" : "°C"))
        device.sendEvent(name: "dewPoint", value: u.dewpt, unit: (units == "e" ? "°F" : "°C"))
        device.sendEvent(name: "heatIndex", value: u.heatIndex, unit: (units == "e" ? "°F" : "°C"))
        device.sendEvent(name: "windChill", value: u.windChill, unit: (units == "e" ? "°F" : "°C"))
        device.sendEvent(name: "windSpeed", value: u.windSpeed, unit: (units == "e" ? "mph" : "km/h"))
        device.sendEvent(name: "windGust", value: u.windGust, unit: (units == "e" ? "mph" : "km/h"))
        device.sendEvent(name: "pressure", value: u.pressure, unit: (units == "e" ? "inHg" : "hPa"))
        device.sendEvent(name: "precipRate", value: u.precipRate, unit: (units == "e" ? "in/hr" : "mm/hr"))
        device.sendEvent(name: "precipTotal", value: u.precipTotal, unit: (units == "e" ? "in" : "mm"))
        device.sendEvent(name: "humidity", value: obs.humidity, unit: "%")
        device.sendEvent(name: "windDirection", value: obs.winddir, unit: "°")
        device.sendEvent(name: "solarRadiation", value: obs.solarRadiation, unit: "w/m²")
        device.sendEvent(name: "uvIndex", value: obs.uv)
        
        def dirs = ["N","NNE","NE","ENE","E","ESE","SE","SSE","S",
                    "SSW","SW","WSW","W","WNW","NW","NNW","N"]
        def idx = (obs.winddir != null) ? Math.round(((obs.winddir.toDouble() % 360) / 22.5)).toInteger() : 0
        device.sendEvent(name: "windDirectionStr", value: dirs.get(idx))
        
        // ADDED ATTRIBUTE: Safely populates the lastUpdateTime field
        def lTime = new Date().format("MM/dd/yyyy hh:mm:ss a", location.timeZone)
        device.sendEvent(name: "lastUpdateTime", value: lTime)
        
        // RESTORED: Fixed back to api.weather.com exactly as your working copy required
        asynchttpGet("parseForecastResponse", [uri: "https://api.weather.com", path: "/v3/wx/forecast/daily/5day", query: [geocode: obs.lat.toString() + "," + obs.lon.toString(), format: "json", units: units, language: "en-US", apiKey: settings.apiKey], timeout: 10], [currentTemp: u.temp])
    } catch (Exception e) { 
        log.error "Current Parse Error: " + e.message 
    }
}
def parseForecastResponse(response, data) {
    if (response.hasError() || response.status != 200) { 
        log.error "Extended forecast download failed with status code: ${response.status}"
        return 
    }
    try {
        def json = parseJson(response.data)
        if (logEnable) log.debug "Forecast Response JSON correctly parsed"
        def units = settings.unitSystem ?: "e"
        def rainUnit = (units == "e" ? "in" : "mm")
        def todayMax = json.temperatureMax.get(0)
        if (todayMax == null && data != null) {
            todayMax = data.currentTemp
        }
        device.sendEvent(name: "forecastTodayMax", value: todayMax)
        device.sendEvent(name: "forecastTodayMin", value: json.temperatureMin.get(0))
        device.sendEvent(name: "forecastTodayNarrative", value: json.narrative.get(0))
        device.sendEvent(name: "forecastTomorrowMax", value: json.temperatureMax.get(1))
        device.sendEvent(name: "forecastTomorrowMin", value: json.temperatureMin.get(1))
        device.sendEvent(name: "forecastTomorrowNarrative", value: json.narrative.get(1))
        
        def rToday = (json.qpf != null) ? json.qpf.get(0) : 0.0
        def rTomorrow = (json.qpf != null) ? json.qpf.get(1) : 0.0
        def rDay2 = (json.qpf != null) ? json.qpf.get(2) : 0.0
        def rDay3 = (json.qpf != null) ? json.qpf.get(3) : 0.0
        
        if (json.daypart != null && json.daypart.get(0) != null) {
            def dp = json.daypart.get(0)
            def names = dp.daypartName
            def pToday = 0, pTomorrow = 0, pDay2 = 0, pDay3 = 0
            def qpToday = 0.0, qpTomorrow = 0.0, qpDay2 = 0.0, qpDay3 = 0.0
            for (int i = 0; i < names.size(); i++) {
                def n = names.get(i)
                if (n == null) continue
                def pct = dp.precipChance.get(i) != null ? dp.precipChance.get(i).toInteger() : 0
                def amt = dp.qpf.get(i) != null ? dp.qpf.get(i).toDouble() : 0.0
                if (n.contains("Today") || n.contains("Tonight")) {
                    if (pct > pToday) pToday = pct
                    qpToday += amt
                } else if (n.contains("Tomorrow")) {
                    if (pct > pTomorrow) pTomorrow = pct
                    qpTomorrow += amt
                } else if (i == 4 || i == 5) {
                    if (pct > pDay2) pDay2 = pct
                    qpDay2 += amt
                } else if (i == 6 || i == 7) {
                    if (pct > pDay3) pDay3 = pct
                    qpDay3 += amt
                }
            }
            device.sendEvent(name: "precipChanceToday", value: pToday, unit: "%")
            device.sendEvent(name: "precipChanceTomorrow", value: pTomorrow, unit: "%")
            device.sendEvent(name: "precipChanceDay2", value: pDay2, unit: "%")
            device.sendEvent(name: "precipChanceDay3", value: pDay3, unit: "%")
            if (rToday == 0.0) rToday = qpToday
            if (rTomorrow == 0.0) rTomorrow = qpTomorrow
            if (rDay2 == 0.0) rDay2 = qpDay2
            if (rDay3 == 0.0) rDay3 = qpDay3
            def txt = dp.wxPhraseLong.get(0) ?: dp.wxPhraseLong.get(1)
            if (txt != null) {
                device.sendEvent(name: "weather", value: txt)
            }
        }
        device.sendEvent(name: "rainExpectedToday", value: rToday, unit: rainUnit)
        device.sendEvent(name: "rainExpectedTomorrow", value: rTomorrow, unit: rainUnit)
        device.sendEvent(name: "rainExpectedDay2", value: rDay2, unit: rainUnit)
        device.sendEvent(name: "rainExpectedDay3", value: rDay3, unit: rainUnit)
    } catch (Exception e) {
        log.error "Forecast Parse Error: " + e.message
    }
}
1 Like

Just making sure you were aware it was out there.
I use most of it for my screensaver on all my dashboard tablets

Yeah, I've found I never look at a weather dashboard if I make one, I just go to a phone app or a webpage. I do put my PWS data on my main dashboard for current weather, including an icon that is generated based on the Ecowitt rain rate from the rain meter, and sky conditions that I derive using the Ecowitt lux sensor.

My main dashboard also has a weather report scroll at the bottom, which combines PWS data and Open Weather Alerts data for the day parts. The real bummer is that UW PWS APIs do not include hourly forecasts from the Weather Channel.

If I wanted to spend $24 year for an accuWeather API subscription, I could get hourly forecasts from that, as I already had AI make an accuWeather driver with my free 2-week trial, but then I found the WU PWS forecasts are free, so I just went with that instead.

1 Like

Yep, that dashboard is a combo of my Ecowitt and weather underground.

I use a combo of Wunderground and my Tempest weatherflow. Works well

Is there anyway to turn off the sound effects on that windy tile? The lightning one is annoying.