Open Meteo API-driven outside temperature and humidity sensor

Hello - i'm starting a new thread from my original NWS sensor given this sensor uses an entirely different API. I am posting my code here for my outside temperature and humidity sensor driver. This pulls data from the open meteo API based on the coordinates specified on the hub.

  • Can report in both F / C
  • Can use the actual temperature or the 'feels like' temperature
  • You can specify a refresh rate and a temperature differential to trigger a change

Hope this is of use to someone.

/*
 *  Outside Temperature Sensor (Open-Meteo)
 *
 *  Hubitat driver that creates a temperature sensor from Open-Meteo's current
 *  weather data for the hub's configured latitude and longitude.
 *
 *  Author: Jon Wallace
 *  Copyright 2025 Jon Wallace
 *  License: MIT
 *
 *  Notes:
 *  - Fahrenheit is the default reporting unit.
 *  - Actual air temperature is reported by default. Apparent temperature can
 *    be selected in preferences.
 *  - Relative humidity is reported when Open-Meteo provides it.
 *  - Requires the hub location coordinates to be configured in Hubitat.
 */

metadata {
  definition (
    name: "Outside Temperature Sensor (Open-Meteo)",
    namespace: "jonw",
    author: "Jon Wallace"
  ) {
    capability "TemperatureMeasurement"
    capability "RelativeHumidityMeasurement"
    capability "Sensor"
    capability "Refresh"
  }

  preferences {
    def lat = location?.latitude
    def lon = location?.longitude
    def hubLocationDescription = (lat != null && lon != null)
      ? "Using hub coordinates: ${lat}, ${lon}"
      : "Hub coordinates are not configured. Set the hub location in Hubitat before this driver can retrieve outside temperature."

    input name: "hubLocationStatus", type: "paragraph", title: hubLocationDescription, displayDuringSetup: true
    input name: "temperatureUnit", type: "enum", title: "Temperature unit", description: "Unit used for temperature events.", options: ["Fahrenheit", "Celsius"], defaultValue: "Fahrenheit"
    input name: "temperatureSource", type: "enum", title: "Reported temperature", description: "Choose whether the temperature attribute uses measured air temperature or the feels-like temperature.", options: ["Actual temperature", "Feels-like temperature"], defaultValue: "Actual temperature"
    input name: "updateFrequency", type: "number", title: "Auto-refresh interval (minutes)", description: "How often the driver refreshes the current outside temperature.", defaultValue: 30, range: "1..1440"
    input name: "minChange", type: "number", title: "Minimum temperature change to report", description: "Suppresses events until the change is at least this amount in the selected unit. Use 0 to report every refresh.", defaultValue: 0.1, range: "0..100"
    input name: "logEnable", type: "bool", title: "Enable debug logging", description: "Log API requests and temperature update decisions.", defaultValue: false
  }
}

// ===== Lifecycle =====

def installed() {
  log.info "Installed Open-Meteo Temperature Sensor"
  initialize()
}

def updated() {
  log.info "Updated settings"
  initialize()
}

def initialize() {
  unschedule()
  scheduleAutoRefresh()
  getOutsideTemperature()
}

// ===== Commands =====

def refresh() {
  getOutsideTemperature()
}

def scheduleAutoRefresh() {
  def minutes = settings?.updateFrequency ?: 30
  minutes = Math.max(1, Math.min(1440, minutes))

  if (logEnable) log.debug "Scheduling auto-refresh every ${minutes} minutes"

  if (minutes == 60) {
    runEvery1Hour(getOutsideTemperature)
  } else if (minutes > 60) {
    runIn(minutes * 60, getOutsideTemperature)
  } else {
    schedule("0 */${minutes} * ? * *", getOutsideTemperature)
  }
}

def getOutsideTemperature() {
  def coords = getHubCoordinates()
  if (!coords) {
    log.error "Hub coordinates are not configured. Set the hub location in Hubitat before retrieving outside temperature."
    scheduleNextRefresh()
    return
  }

  def lat = coords.lat
  def lon = coords.lon
  def url = "https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&current=temperature_2m,apparent_temperature,relative_humidity_2m"

  if (logEnable) log.debug "Requesting temperature from: ${url}"

  try {
    httpGet([uri: url, contentType: "application/json"]) { resp ->
      if (resp.status == 200) {
        def data = resp.data
        def tempC = data?.current?.temperature_2m
        def apparentTempC = data?.current?.apparent_temperature
        def humidity = data?.current?.relative_humidity_2m
        def selectedSource = settings?.temperatureSource ?: "Actual temperature"
        def sourceTempC = selectedSource == "Feels-like temperature" ? apparentTempC : tempC
        def sourceLabel = selectedSource == "Feels-like temperature" ? "feels-like temperature" : "actual temperature"

        if (sourceTempC != null) {
          def selectedUnit = settings?.temperatureUnit ?: "Fahrenheit"
          def unit = selectedUnit == "Celsius" ? "°C" : "°F"
          def tempValue = (
            selectedUnit == "Celsius"
              ? (sourceTempC as BigDecimal).setScale(1, BigDecimal.ROUND_HALF_UP)
              : (((sourceTempC as BigDecimal) * 9 / 5) + 32).setScale(1, BigDecimal.ROUND_HALF_UP)
          )
          def currentVal = device.currentValue("temperature")
          def currentTemp = currentVal != null ? new BigDecimal(currentVal.toString()) : null
          def threshold = settings?.minChange != null ? (settings.minChange as BigDecimal) : new BigDecimal("0.1")

          if (currentTemp == null || (tempValue - currentTemp).abs() >= threshold) {
            sendEvent(name: "temperature", value: tempValue, unit: unit)
            if (logEnable) log.debug "Temperature updated to ${tempValue}${unit} from ${sourceLabel}"
          } else if (logEnable) {
            log.debug "Temperature change (${tempValue}${unit}) within threshold (${threshold}${unit}); event not sent."
          }
        } else {
          log.warn "${selectedSource} data missing in Open-Meteo response"
        }

        if (humidity != null) {
          sendEvent(name: "humidity", value: humidity as BigDecimal, unit: "%")
          if (logEnable) log.debug "Humidity updated to ${humidity}%"
        } else {
          log.warn "Humidity data missing in Open-Meteo response"
        }
      } else {
        log.error "Failed to get temperature: ${resp.status}"
      }
    }
  } catch (Exception e) {
    log.error "Error fetching temperature: ${e.message}"
  }

  scheduleNextRefresh()
}

// ===== Helpers =====

def scheduleNextRefresh() {
  def minutes = settings?.updateFrequency != null ? settings.updateFrequency as Integer : 30
  minutes = Math.max(1, Math.min(1440, minutes))

  if (minutes > 60) {
    runIn(minutes * 60, getOutsideTemperature)
  }
}

def getHubCoordinates() {
  def lat = location?.latitude
  def lon = location?.longitude
  return (lat != null && lon != null) ? [lat: lat, lon: lon] : null
}

EDIT: Removed sections from preferences based on feedback from @thebearmay

1 Like

Doesn' t hurt anything I guess, but in general sections aren't needed/used in device drivers...

Yeah - I have been messing with a couple of apps recently so this was mostly out of habit - that's for the input.

1 Like