I have been using the Open Weather Alerts driver to check rain for my irrigation scheduler, but I have not been impressed with the accuracy of the forecasts.
Since AccuWeather is considered the most accurate weather site, I wanted a driver that would use the AccuWeather api data.
I literally spent about 30 minutes with AI to create this driver. I made a developer account on the AccuWeather site, and started a free trial. The free trial does not need a credit card.
A personal account is then $2/month for personal access. So, $24/year is not too bad...and you get 15,000 calls a month.
I had DeepSeek write a driver. It took only about six iterations to get it working how I wanted.
If interested, here is the driver:
/**
* AccuWeather Driver for Hubitat Elevation Hub
* v1.2.0 – supports both temperature (°C/°F) and rain (mm/in) unit preferences.
*/
import groovy.json.JsonSlurper
metadata {
definition(
name: "AccuWeather Driver",
namespace: "hubitat.accuweather",
author: "Hubitat Community",
importUrl: "https://raw.githubusercontent.com/hubitat/accuweather-driver/main/accuweather-driver.groovy"
) {
capability "TemperatureMeasurement"
capability "RelativeHumidityMeasurement"
capability "IlluminanceMeasurement"
capability "Sensor"
attribute "weatherCondition", "string"
attribute "weatherIcon", "number"
attribute "windSpeed", "number"
attribute "windDirection", "string"
attribute "pressure", "number"
attribute "pressureTrend", "string"
attribute "visibility", "number"
attribute "uvIndex", "number"
attribute "realFeel", "number"
attribute "precipitationProbability", "number"
attribute "forecastHigh", "number"
attribute "forecastLow", "number"
attribute "forecastDay1", "string"
attribute "forecastDay2", "string"
attribute "forecastDay3", "string"
attribute "forecastDay4", "string"
attribute "forecastDay5", "string"
attribute "hourlyForecast", "string"
attribute "lastUpdate", "string"
attribute "apiCallsRemaining", "number"
attribute "apiError", "string"
attribute "rainProbabilityDay1", "number"
attribute "rainProbabilityDay2", "number"
attribute "rainProbabilityDay3", "number"
attribute "rainProbabilityDay4", "number"
attribute "rainProbabilityDay5", "number"
attribute "rainAmountDay1", "number"
attribute "rainAmountDay2", "number"
attribute "rainAmountDay3", "number"
attribute "rainAmountDay4", "number"
attribute "rainAmountDay5", "number"
command "refresh"
command "poll"
command "resetCounters"
command "reschedule"
}
preferences {
section("AccuWeather API Configuration") {
input(
name: "apiKey",
type: "text",
title: "API Key",
description: "Your AccuWeather API key",
required: true,
displayDuringSetup: true
)
input(
name: "locationKey",
type: "text",
title: "Location Key",
description: "AccuWeather location key (e.g., 349727 for NYC)",
required: true,
displayDuringSetup: true
)
}
section("Polling Configuration") {
input(
name: "pollingInterval",
type: "number",
title: "Polling Interval (seconds)",
description: "How often to poll (300‑3600 seconds)",
range: "300..3600",
defaultValue: 900,
displayDuringSetup: true
)
input(
name: "enableCurrentConditions",
type: "bool",
title: "Enable Current Conditions",
defaultValue: true,
displayDuringSetup: true
)
input(
name: "enableDailyForecast",
type: "bool",
title: "Enable 5‑Day Forecast",
defaultValue: true,
displayDuringSetup: true
)
input(
name: "enableHourlyForecast",
type: "bool",
title: "Enable 12‑Hour Forecast",
defaultValue: false,
displayDuringSetup: true
)
}
section("Unit Preferences") {
input(
name: "tempUnit",
type: "enum",
title: "Temperature Unit",
options: ["C", "F"],
defaultValue: "F",
displayDuringSetup: true
)
input(
name: "rainUnit",
type: "enum",
title: "Rain Amount Unit",
options: ["mm", "in"],
defaultValue: "mm",
displayDuringSetup: true
)
}
section("Advanced") {
input(
name: "logEnable",
type: "bool",
title: "Enable Debug Logging",
defaultValue: false,
displayDuringSetup: true
)
input(
name: "apiCallsResetDay",
type: "number",
title: "API Call Counter Reset Day",
range: "1..28",
defaultValue: 1,
displayDuringSetup: false
)
}
}
}
// ============================================================================
// Lifecycle Methods
// ============================================================================
def installed() {
log.info "AccuWeather Driver installed"
initialize()
}
def updated() {
log.info "AccuWeather Driver updated – rescheduling"
unschedule()
initialize()
}
def initialize() {
if (!apiKey || !locationKey) {
log.warn "API Key or Location Key not configured."
return
}
def intervalSeconds = (pollingInterval ?: 900) as Integer
def intervalMinutes = Math.max(1, Math.round(intervalSeconds / 60)) as Integer
def cron = "0 */${intervalMinutes} * * * ?"
schedule(cron, "pollWeather")
runIn(2, "pollWeather")
def resetDay = (apiCallsResetDay ?: 1) as Integer
schedule("0 0 0 ${resetDay} * ? *", "resetApiCounter")
log.info "Polling scheduled with cron '${cron}' (${intervalSeconds} seconds)"
}
// ============================================================================
// Polling Methods
// ============================================================================
def pollWeather() {
if (!apiKey || !locationKey) {
log.warn "Cannot poll: API Key or Location Key not configured"
return
}
logDebug "Starting weather poll"
def callsMade = 0
sendEvent(name: "apiError", value: "")
if (enableCurrentConditions != false) {
fetchCurrentConditions { callsMade++ }
}
if (enableDailyForecast != false) {
fetchDailyForecast { callsMade++ }
}
if (enableHourlyForecast != false) {
fetchHourlyForecast { callsMade++ }
}
sendEvent(name: "lastUpdate", value: new Date().format("yyyy-MM-dd HH:mm:ss"))
updateApiCounter(callsMade)
logDebug "Poll completed - ${callsMade} API calls made"
}
// ============================================================================
// API Fetch Methods
// ============================================================================
def fetchCurrentConditions(closure = null) {
def endpoint = "https://dataservice.accuweather.com/currentconditions/v1/${locationKey}"
def params = [
uri: endpoint,
query: [apikey: apiKey, details: "true"],
timeout: 15
]
logDebug "Fetching Current Conditions from ${endpoint}"
try {
httpGet(params) { resp ->
if (resp.status == 200 && resp.data) {
parseCurrentConditions(resp.data)
if (closure) closure()
} else {
log.warn "Current Conditions failed: HTTP ${resp.status}"
sendEvent(name: "apiError", value: "Current Conditions: HTTP ${resp.status}")
}
}
} catch (Exception e) {
log.error "Error fetching Current Conditions: ${e.message}"
sendEvent(name: "apiError", value: "Current Conditions: ${e.message}")
}
}
def fetchDailyForecast(closure = null) {
def endpoint = "https://dataservice.accuweather.com/forecasts/v1/daily/5day/${locationKey}"
def params = [
uri: endpoint,
query: [apikey: apiKey, details: "true", metric: "true"],
timeout: 15
]
logDebug "Fetching 5‑Day Forecast from ${endpoint}"
try {
httpGet(params) { resp ->
if (resp.status == 200 && resp.data) {
parseDailyForecast(resp.data)
if (closure) closure()
} else {
log.warn "Daily Forecast failed: HTTP ${resp.status}"
sendEvent(name: "apiError", value: "Daily Forecast: HTTP ${resp.status}")
}
}
} catch (Exception e) {
log.error "Error fetching Daily Forecast: ${e.message}"
sendEvent(name: "apiError", value: "Daily Forecast: ${e.message}")
}
}
def fetchHourlyForecast(closure = null) {
def endpoint = "https://dataservice.accuweather.com/forecasts/v1/hourly/12hour/${locationKey}"
def params = [
uri: endpoint,
query: [apikey: apiKey, details: "true", metric: "true"],
timeout: 15
]
logDebug "Fetching 12‑Hour Forecast from ${endpoint}"
try {
httpGet(params) { resp ->
if (resp.status == 200 && resp.data) {
parseHourlyForecast(resp.data)
if (closure) closure()
} else {
log.warn "Hourly Forecast failed: HTTP ${resp.status}"
sendEvent(name: "apiError", value: "Hourly Forecast: HTTP ${resp.status}")
}
}
} catch (Exception e) {
log.error "Error fetching Hourly Forecast: ${e.message}"
sendEvent(name: "apiError", value: "Hourly Forecast: ${e.message}")
}
}
// ============================================================================
// Parsing Methods – with temperature conversion
// ============================================================================
// Helper: convert Celsius to Fahrenheit
private def cToF(celsius) {
if (celsius == null) return null
return (celsius * 9 / 5) + 32
}
// Helper: send temperature event with proper unit
private def sendTempEvent(name, valueCelsius, unitPreference) {
if (valueCelsius == null) return
def value = valueCelsius
def unit = "°C"
if (unitPreference == "F") {
value = cToF(valueCelsius)
unit = "°F"
}
// Round to 1 decimal
value = Math.round(value * 10) / 10
sendEvent(name: name, value: value, unit: unit)
}
def parseCurrentConditions(data) {
try {
if (!(data instanceof List) || data.size() == 0) {
log.warn "Current Conditions: empty or invalid response"
return
}
def conditions = data[0]
if (!conditions) return
def tempUnit = (tempUnit ?: "F") as String
// Temperature
def tempC = conditions.Temperature?.Metric?.Value
sendTempEvent("temperature", tempC, tempUnit)
// RealFeel
def realFeelC = conditions.RealFeelTemperature?.Metric?.Value
sendTempEvent("realFeel", realFeelC, tempUnit)
// Humidity
def humidity = conditions.RelativeHumidity
if (humidity != null) sendEvent(name: "humidity", value: humidity, unit: "%")
// Weather condition text & icon
def weatherText = conditions.WeatherText
if (weatherText) sendEvent(name: "weatherCondition", value: weatherText)
def icon = conditions.WeatherIcon
if (icon != null) sendEvent(name: "weatherIcon", value: icon)
// Wind
def windSpeed = conditions.Wind?.Speed?.Metric?.Value
if (windSpeed != null) sendEvent(name: "windSpeed", value: windSpeed)
def windDir = conditions.Wind?.Direction?.English
if (windDir) sendEvent(name: "windDirection", value: windDir)
// Pressure
def pressure = conditions.Pressure?.Metric?.Value
if (pressure != null) sendEvent(name: "pressure", value: pressure)
def pressureTrend = conditions.PressureTendency?.LocalizedText
if (pressureTrend) sendEvent(name: "pressureTrend", value: pressureTrend)
// Visibility
def visibility = conditions.Visibility?.Metric?.Value
if (visibility != null) sendEvent(name: "visibility", value: visibility)
// UV Index & Illuminance
def uvIndex = conditions.UVIndex
if (uvIndex != null) {
sendEvent(name: "uvIndex", value: uvIndex)
sendEvent(name: "illuminance", value: uvIndex * 1000, unit: "lux")
}
// Precipitation probability
def precipProb = conditions.PrecipitationProbability
if (precipProb != null) sendEvent(name: "precipitationProbability", value: precipProb)
logDebug "Current Conditions parsed successfully"
} catch (Exception e) {
log.error "Error parsing Current Conditions: ${e.message}"
if (logEnable) log.debug "Raw data: ${data}"
}
}
def parseDailyForecast(data) {
try {
if (!data) {
log.warn "Daily Forecast: null response"
return
}
if (data.Code || data.Message) {
log.warn "Daily Forecast API error: ${data.Code} - ${data.Message}"
sendEvent(name: "apiError", value: "Daily Forecast: ${data.Message}")
return
}
def dailyForecasts = data.DailyForecasts
if (!dailyForecasts || dailyForecasts.size() == 0) {
log.warn "Daily Forecast: no 'DailyForecasts' array"
return
}
def tempUnit = (tempUnit ?: "F") as String
def rainUnitPref = (rainUnit ?: "mm") as String
def isInches = (rainUnitPref == "in")
// Process first day for primary forecast attributes
def today = dailyForecasts[0]
if (today) {
def highC = today.Temperature?.Maximum?.Value
sendTempEvent("forecastHigh", highC, tempUnit)
def lowC = today.Temperature?.Minimum?.Value
sendTempEvent("forecastLow", lowC, tempUnit)
def dayText = today.Day?.IconPhrase
if (dayText) sendEvent(name: "weatherCondition", value: dayText)
def dayIcon = today.Day?.Icon
if (dayIcon != null) sendEvent(name: "weatherIcon", value: dayIcon)
def precipProb = today.Day?.PrecipitationProbability
if (precipProb != null) sendEvent(name: "precipitationProbability", value: precipProb)
}
// Process all 5 days
dailyForecasts.eachWithIndex { day, index ->
if (index < 5) {
def dayNum = index + 1
// Date abbreviation
def dateStr = day.Date
def shortDay = "???"
try {
def parsedDate = Date.parse("yyyy-MM-dd'T'HH:mm:ssX", dateStr)
shortDay = parsedDate.format("EEE")
} catch (Exception e) {
logDebug "Could not parse date '${dateStr}': ${e.message}"
}
// High/Low – convert if needed
def highC = day.Temperature?.Maximum?.Value
def lowC = day.Temperature?.Minimum?.Value
def highDisplay = highC
def lowDisplay = lowC
def tempUnitSymbol = "°C"
if (tempUnit == "F") {
highDisplay = cToF(highC)
lowDisplay = cToF(lowC)
tempUnitSymbol = "°F"
}
// Round
highDisplay = highDisplay != null ? Math.round(highDisplay * 10) / 10 : null
lowDisplay = lowDisplay != null ? Math.round(lowDisplay * 10) / 10 : null
def condition = day.Day?.IconPhrase ?: "Unknown"
def summary = "${shortDay}: ${condition}, ${highDisplay}${tempUnitSymbol}/${lowDisplay}${tempUnitSymbol}"
sendEvent(name: "forecastDay${dayNum}", value: summary)
// Rain Probability
def rainProb = day.Day?.RainProbability ?: day.Day?.PrecipitationProbability
logDebug "Day ${dayNum} - RainProbability: ${rainProb}"
if (rainProb != null) {
sendEvent(name: "rainProbabilityDay${dayNum}", value: rainProb)
}
// Rain Amount
def rainAmountMm = day.Day?.Rain?.Value ?: day.Day?.TotalLiquid?.Value
logDebug "Day ${dayNum} - RainAmount (mm): ${rainAmountMm}"
if (rainAmountMm != null) {
def finalAmount = rainAmountMm
def unitLabel = "mm"
if (isInches) {
finalAmount = rainAmountMm / 25.4
unitLabel = "in"
}
finalAmount = Math.round(finalAmount * 100) / 100
sendEvent(name: "rainAmountDay${dayNum}", value: finalAmount, unit: unitLabel)
logDebug "Day ${dayNum} - RainAmount (${unitLabel}): ${finalAmount}"
} else {
logDebug "Day ${dayNum} - RainAmount is null"
}
}
}
logDebug "5‑Day Forecast parsed successfully"
} catch (Exception e) {
log.error "Error parsing Daily Forecast: ${e.message}"
if (logEnable) log.debug "Raw Daily Forecast data: ${data}"
}
}
def parseHourlyForecast(data) {
try {
if (!data || data.size() == 0) {
log.warn "Hourly Forecast: no data"
return
}
if (data.Code || data.Message) {
log.warn "Hourly Forecast API error: ${data.Code} - ${data.Message}"
sendEvent(name: "apiError", value: "Hourly Forecast: ${data.Message}")
return
}
def hourlyData = data.collect { hour ->
[
time: hour.DateTime,
temp: hour.Temperature?.Value,
condition: hour.IconPhrase,
icon: hour.WeatherIcon,
precipProb: hour.PrecipitationProbability,
windSpeed: hour.Wind?.Speed?.Value,
humidity: hour.RelativeHumidity
]
}
def json = new groovy.json.JsonOutput().toJson(hourlyData)
sendEvent(name: "hourlyForecast", value: json)
logDebug "12‑Hour Forecast parsed successfully"
} catch (Exception e) {
log.error "Error parsing Hourly Forecast: ${e.message}"
if (logEnable) log.debug "Raw Hourly data: ${data}"
}
}
// ============================================================================
// API Counter Management
// ============================================================================
def updateApiCounter(callsMade) {
try {
def stateKey = "apiCallsMonth"
def currentCalls = state[stateKey] ?: 0
def newTotal = currentCalls + callsMade
if (newTotal > 14000) log.warn "API calls this month: ${newTotal} - approaching 15,000 limit!"
state[stateKey] = newTotal
sendEvent(name: "apiCallsRemaining", value: Math.max(0, 15000 - newTotal))
} catch (Exception e) {
log.error "Error updating API counter: ${e.message}"
}
}
def resetApiCounter() {
state["apiCallsMonth"] = 0
sendEvent(name: "apiCallsRemaining", value: 15000)
log.info "API call counter reset"
}
// ============================================================================
// Utility & Commands
// ============================================================================
def logDebug(message) {
if (logEnable) log.debug message
}
def refresh() { log.info "Manual refresh"; pollWeather() }
def poll() { log.info "Manual poll"; pollWeather() }
def resetCounters() { resetApiCounter() }
def reschedule() { log.info "Manually rescheduling"; unschedule(); initialize() }
def getApiCalls() { return state["apiCallsMonth"] ?: 0 }
Edit: I missed specifying units. The driver is updated now to let you choose in/mm rain, and F/C temp in preferences.








