I wouldn't try to send them off the hub for processing, that can all happen in a simple app and a device. That is a good automation for AI to create, to make you an actual rainMeter device, based on your contact sensor rain counter.
An app can subscribe to the contact sensor and count every open event per hour, and daily, and give you rain rate every time it updates based on a setting for what one open event is for rain inches. It can put those values in a virtual child device in the form of attributes, such as rainHourly, rainDaily, and rainRate. Then you can retrieve those values live in other automations.
I took a minute to write a query and get it started, if interested. Here is the code if you want to try testing it and feeding back to AI to get it working. I may play around with it myself when I have some time and get AI to fix up any issues.
/**
* Rain Gauge Counter
*
* Counts contact openings as rain tips, accumulates hourly and daily totals,
* and computes the current rain rate. Updates a child device with attributes:
* rainHourly, rainDaily, and rainRate.
*
* Author: Your Name
* Date: 2026-06-19
*/
definition(
name: "Rain Gauge Counter",
namespace: "com.example",
author: "Your Name",
description: "Counts contact opens as rain tips and calculates rain totals and rate.",
category: "Weather",
iconUrl: "",
iconX2Url: ""
)
preferences {
input name: "contactSensor", type: "capability.contactSensor", title: "Contact Sensor", required: true
input name: "inchesPerEvent", type: "decimal", title: "Inches of rain per open event", defaultValue: 0.01, required: true
}
def installed() {
initialize()
}
def updated() {
unsubscribe()
initialize()
}
def initialize() {
// Subscribe to the contact sensor's 'contact' event
subscribe(contactSensor, "contact", contactHandler)
// Schedule resets at the top of every hour (0 minutes)
schedule("0 0 * * * ?", hourlyReset)
// Create the child device (using our custom driver)
createChildDevice()
// Initialize state variables if they are null
if (state.hourlyTotal == null) state.hourlyTotal = 0.0
if (state.dailyTotal == null) state.dailyTotal = 0.0
if (state.lastEventTime == null) state.lastEventTime = null
if (state.lastRate == null) state.lastRate = 0.0
// Set initial values on the child device
updateChildDevice(state.hourlyTotal, state.dailyTotal, state.lastRate)
}
/**
* Create a child device using our custom driver.
* Driver namespace: "com.example", driver name: "Rain Meter Child"
*/
def createChildDevice() {
def networkId = "rainMeter-${app.id}"
def child = getChildDevice(networkId)
if (!child) {
try {
child = addChildDevice("com.example", "Rain Meter Child", networkId, [
label: "Rain Meter",
isComponent: false
])
log.info "Child device 'Rain Meter' created."
} catch (e) {
log.error "Failed to create child device: ${e.message}"
}
}
}
/**
* Handler for contact sensor events.
* Only processes 'open' events as rain tips.
*/
def contactHandler(evt) {
if (evt.value == "open") {
def now = new Date().time
def inches = inchesPerEvent.toDouble()
// Update totals
state.hourlyTotal = (state.hourlyTotal ?: 0.0) + inches
state.dailyTotal = (state.dailyTotal ?: 0.0) + inches
// Compute rain rate (inches per hour) based on time since last tip
def rate = 0.0
if (state.lastEventTime != null) {
def deltaMs = now - state.lastEventTime
if (deltaMs > 0) {
def deltaHours = deltaMs / 3600000.0
rate = inches / deltaHours
} else {
// If zero time elapsed, keep previous rate or set to 0
rate = state.lastRate ?: 0.0
}
} else {
// First event: rate is undefined, keep 0
rate = 0.0
}
state.lastEventTime = now
state.lastRate = rate
// Update child device
updateChildDevice(state.hourlyTotal, state.dailyTotal, rate)
}
}
/**
* Scheduled job that runs at the top of every hour.
* Resets the hourly total, and also resets the daily total at midnight.
*/
def hourlyReset() {
state.hourlyTotal = 0.0
def now = new Date()
// Check if it's exactly midnight
if (now.getHours() == 0 && now.getMinutes() == 0) {
state.dailyTotal = 0.0
}
// Keep the last computed rate, or set to 0 if not available
def rate = state.lastRate ?: 0.0
updateChildDevice(state.hourlyTotal, state.dailyTotal, rate)
}
/**
* Sends events to the child device to update its attributes.
*/
def updateChildDevice(hourly, daily, rate) {
def networkId = "rainMeter-${app.id}"
def child = getChildDevice(networkId)
if (child) {
child.sendEvent(name: "rainHourly", value: hourly, unit: "in")
child.sendEvent(name: "rainDaily", value: daily, unit: "in")
child.sendEvent(name: "rainRate", value: rate, unit: "in/hr")
} else {
log.warn "Child device not found; attempting to recreate."
createChildDevice()
// Retry once
child = getChildDevice(networkId)
if (child) {
child.sendEvent(name: "rainHourly", value: hourly, unit: "in")
child.sendEvent(name: "rainDaily", value: daily, unit: "in")
child.sendEvent(name: "rainRate", value: rate, unit: "in/hr")
}
}
}
/**
* Rain Meter Child Device
*
* Provides attributes for rainHourly, rainDaily, and rainRate.
* Receives updates from the parent Rain Gauge Counter app.
*
* Author: Your Name
* Date: 2026-06-19
*/
metadata {
definition(name: "Rain Meter Child", namespace: "com.example", author: "Your Name") {
capability "Sensor"
// Custom attributes
attribute "rainHourly", "number"
attribute "rainDaily", "number"
attribute "rainRate", "number"
// Optionally display these in the UI
command "refresh"
}
// Optional: tile definitions for a clean device page
tiles(scale: 2) {
multiAttributeTile(name:"rainTotals", type:"generic", width:6, height:4) {
tileAttribute("device.rainHourly", key: "PRIMARY_CONTROL") {
attributeState("number", label:'${value} in/hr', icon:"st.Weather.weather2")
}
tileAttribute("device.rainDaily", key: "SECONDARY_CONTROL") {
attributeState("number", label:'Daily: ${value} in')
}
}
valueTile("rainRate", "device.rainRate", width:2, height:2) {
state("number", label:'Rate: ${value} in/hr')
}
main "rainTotals"
details(["rainTotals", "rainRate"])
}
}
/**
* Called when the device is installed or updated.
* Not strictly required for receiving events, but good practice.
*/
def installed() {
initialize()
}
def updated() {
initialize()
}
def initialize() {
// Set default values if they haven't been set yet
if (device.currentValue("rainHourly") == null) {
sendEvent(name: "rainHourly", value: 0.0, unit: "in")
}
if (device.currentValue("rainDaily") == null) {
sendEvent(name: "rainDaily", value: 0.0, unit: "in")
}
if (device.currentValue("rainRate") == null) {
sendEvent(name: "rainRate", value: 0.0, unit: "in/hr")
}
}
/**
* Optional refresh command – pushes current states again.
*/
def refresh() {
def hourly = device.currentValue("rainHourly") ?: 0.0
def daily = device.currentValue("rainDaily") ?: 0.0
def rate = device.currentValue("rainRate") ?: 0.0
sendEvent(name: "rainHourly", value: hourly, unit: "in")
sendEvent(name: "rainDaily", value: daily, unit: "in")
sendEvent(name: "rainRate", value: rate, unit: "in/hr")
}
// No parse() needed – events are pushed from the parent via sendEvent.