Thermacell LIV Mosquito Repeller β Cloud Integration (ESP Rainmaker)
Overview
This is to solve the problem of integrating Thermacell LIV v2 mosquito controller into Hubitat platform. Previously, there was no way to connect Thermocell to Hubatat and you had to control it separately with Thermocell LIV app.
(Supposedly, Thermacell integrates with Amazon or Google via ESP Rainmaker. But did not work for me. And Ayla Networks backend for v1 controller is deprecated.)
This is a two-driver integration that brings Thermacell LIV mosquito repeller hubs into Hubitat as native devices. It talks directly to the Thermacell cloud API (ESP Rainmaker, hosted at api.iot.thermacell.com) β no hub or middleman required. All your LIV hubs are discovered automatically from a single parent device.
Each hub appears in Hubitat as a Switch with three extra attributes:
-
repellerStatus β Protected, Warming Up, Off, Error
-
refillLife β hours remaining on the repellent cartridge
-
connectivity β online / offline
Credentials note: Your Thermacell email and password are entered once in the manager's preferences. The driver exchanges them for session tokens immediately, then clears the credentials from preferences β only the tokens are stored in device state. This matches the same pattern used by other cloud drivers in the Hubitat ecosystem.
Installation
Install both drivers via Drivers Code β + New Driver. Install the child driver first, then the manager. When pasting, make sure to clear all existing content in the editor before pasting β the code must start on line 1.
Driver 1 of 2 β Thermacell LIV Hub (child driver, install this first)
metadata {
definition(name: "Thermacell LIV Hub", namespace: "thermacell", author: "thermacell") {
capability "Switch"
capability "Sensor"
attribute "repellerStatus", "string"
attribute "refillLife", "number"
attribute "connectivity", "string"
}
}
// Called by parent on each poll
def updateState(connected, enabled, status, refill) {
sendEvent(name: "switch", value: enabled ? "on" : "off")
sendEvent(name: "connectivity", value: connected ? "online" : "offline")
sendEvent(name: "repellerStatus", value: status ?: "Unknown")
if (refill != null) sendEvent(name: "refillLife", value: refill as int, unit: "hours")
}
def on() {
sendEvent(name: "switch", value: "on")
parent.setPower(nodeId(), true)
}
def off() {
sendEvent(name: "switch", value: "off")
parent.setPower(nodeId(), false)
}
def nodeId() {
return device.deviceNetworkId.replaceFirst("thermacell-", "")
}
Driver 2 of 2 β Thermacell LIV Manager (parent driver)
metadata {
definition(name: "Thermacell LIV Manager", namespace: "thermacell", author: "thermacell") {
capability "Initialize"
capability "Refresh"
command "disconnect"
}
preferences {
input name: "email", type: "string", title: "Thermacell Email", required: false
input name: "password", type: "password", title: "Thermacell Password", required: false
input name: "pollMins", type: "enum", title: "Poll Interval",
options: \["1", "5"\], defaultValue: "1", required: true
input name: "logEnable", type: "bool", title: "Enable debug logging", defaultValue: false
}
}
def installed() { initialize() }
def updated() {
unschedule()
initialize()
}
def initialize() {
if (settings.email && settings.password) {
if (!doLogin(settings.email, settings.password)) {
log.error "Thermacell: login failed β check credentials"
return
}
device.updateSetting("email", \[value: "", type: "string"\])
device.updateSetting("password", \[value: "", type: "password"\])
}
if (!state.accessToken) {
log.warn "Thermacell: not connected β enter email and password in preferences"
return
}
if (settings.pollMins == "5") runEvery5Minutes(refresh)
else runEvery1Minute(refresh)
refresh()
}
def doLogin(username, password) {
def ok = false
try {
httpPostJson(\[
uri: "https://api.iot.thermacell.com",
path: "/v1/login2",
timeout: 10,
body: \[user_name: username, password: password\],
\]) { resp ->
storeTokens(resp.data)
ok = true
}
} catch (e) {
log.error "Thermacell login error: ${e.message}"
}
return ok
}
def doRefresh() {
def ok = false
try {
httpPostJson(\[
uri: "https://api.iot.thermacell.com",
path: "/v1/login2",
timeout: 10,
body: \[refreshtoken: state.refreshToken\],
\]) { resp ->
storeTokens(resp.data)
ok = true
}
} catch (e) {
log.warn "Thermacell token refresh failed: ${e.message}"
}
return ok
}
def storeTokens(data) {
state.accessToken = data.accesstoken
state.refreshToken = data.refreshtoken
state.tokenExpiry = now() + (50 \* 60 \* 1000)
if (logEnable) log.debug "Thermacell: tokens stored"
}
def ensureToken() {
if (!state.accessToken) { log.warn "Thermacell: not connected"; return false }
if (now() >= (state.tokenExpiry ?: 0)) return doRefresh()
return true
}
def refresh() {
if (!ensureToken()) return
def nodes = \[\]
try {
httpGet(\[
uri: "https://api.iot.thermacell.com",
path: "/v1/user/nodes",
query: \[node_details: "true"\],
headers: \[Authorization: state.accessToken\],
timeout: 10,
\]) { resp ->
nodes = resp.data.node_details ?: \[\]
}
} catch (e) {
log.error "Thermacell getNodes failed: ${e.message}"
return
}
nodes.each { node ->
def nodeId = node.id as String
def hub = node.params?.get("LIV Hub") ?: \[:\]
def connected = node.status?.connectivity?.connected ?: false
def enabled = hub\["Enable Repellers"\] ?: false
def status = hub\["Status"\] ?: (enabled ? "On" : "Off")
def refill = hub\["Refill Life"\]
def dni = "thermacell-${nodeId}"
def child = getChildDevice(dni)
if (!child) {
def label = node.config?.info?.name
?: node.config?.devices?.getAt(0)?.name
?: "LIV Hub"
child = addChildDevice("thermacell", "Thermacell LIV Hub", dni,
\[name: label, label: label, isComponent: false\])
log.info "Thermacell: added '${label}' (${nodeId})"
}
child.updateState(connected, enabled, status, refill)
}
}
def setPower(nodeId, enabled) {
if (!ensureToken()) return
try {
httpPut(\[
uri: "https://api.iot.thermacell.com",
path: "/v1/user/nodes/params",
requestContentType: "application/json",
headers: \[Authorization: state.accessToken\],
body: \[\[node_id: nodeId, payload: \["LIV Hub": \["Enable Repellers": enabled\]\]\]\],
timeout: 10,
\]) { resp ->
if (logEnable) log.debug "Thermacell setPower ${nodeId} β ${enabled}: ${resp.status}"
}
} catch (e) {
log.error "Thermacell setPower failed: ${e.message}"
}
}
def disconnect() {
unschedule()
state.clear()
log.info "Thermacell: disconnected"
}
Setup
1. Go to Devices β Add Device β Virtual
2. Name it (e.g. "Thermacell Manager"), set Type to Thermacell LIV Manager, save
3. On the device page, if preferences show "default state" instead of the email/password fields, check that the Type is correctly set to Thermacell LIV Manager
4. Enter your Thermacell email and password and hit Save Preferences
5. Watch Logs β the manager will log in, clear your credentials, and create a child device for each LIV hub on your account
Known limitations
-
Cloud dependent β requires internet access to api.iot.thermacell.com
-
Tokens are valid for ~30 days; if the refresh token expires you'll need to re-enter credentials in preferences
-
Field names (LIV Hub, Enable Repellers, etc.) are based on reverse-engineering the ESP Rainmaker API β please report in this thread if your hub uses different parameter names and I'll update the driver
Happy to hear feedback, especially from anyone with the older LIV v1 hardware (Ayla Networks backend) β that would need a separate driver.