Has anyone figured out the API endpoints for the Fi dog collar? I saw there was an old post discussing it but nothing recently.
never knew that but following now... I have two Fi 3+ collars and willing to join the testing if something comes along.
I use Tasker on my phone to watch for the various notifications Fi creates, and then it sends appropriate messages to HE which in turn sends announcements via Echo Speaks. Works well enough, although a direct API implementation would be better.
There is an HA integration, should you happen to be using HA and HADB with Hubitat.
I don't have one of these collars, but I just spent two minutes having Deepseek AI create a driver for Hubitat, based on the HA integration. Deepseek has been doing a great job creating Hubitat drivers for me for other things.
Seems some people don't trust putting AI drivers on their hubs, but the sandbox that community drivers run in provides plenty of safeguards to catch errant code issues, so I'm not clear on that paranoia people have.
This is the driver Deepseek created should you want to try it. If this first pass AI code does not work, just start a new session in Deepseek by pasting the code back into a new chat, and then paste in any error logs it generated and have it fix it. A few, or several, iterations of posting error logs back to the chat gets me a working driver.
Edit: So I posted this code without seeing it was only a framework for writing a driver. I had the session write the whole driver now, updated below.
Fi Collar Driver for Hubitat
/*
* Fi Collar Driver for Hubitat Elevation
* Based on Home Assistant tryfi integration
*
* Capabilities: Battery, Presence, Switch (light), Switch (lost dog mode)
* Attributes: steps, distance, base status, last location
*/
#include "fi_api_client"
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
metadata {
definition (name: "Fi Collar", namespace: "fi.integration", author: "Hubitat Community") {
capability "Battery"
capability "PresenceSensor"
capability "Switch" // Collar light
capability "Refresh"
// Second switch capability for Lost Dog Mode (custom command)
capability "Switch" // Will be used as lostDogMode, but we rename later
attribute "stepsDaily", "number"
attribute "stepsWeekly", "number"
attribute "stepsMonthly", "number"
attribute "distanceDaily", "number"
attribute "distanceWeekly", "number"
attribute "distanceMonthly", "number"
attribute "baseStatus", "string" // "online" or "offline"
attribute "lastLocation", "string" // "Home" or "Away" or lat/lon summary
attribute "lostDogMode", "string" // "on" / "off"
command "setLostDogMode", [[name:"status", type:"ENUM", constraints:["ON","OFF"]]]
command "refresh"
}
preferences {
input "username", "text", title: "Fi Email", required: true
input "password", "password", title: "Fi Password", required: true
input "pollInterval", "number", title: "Polling Interval (seconds)", defaultValue: 60, required: true
input "logEnable", "bool", title: "Enable debug logging", defaultValue: true
}
}
// --------------------------------------------------------------
// Lifecycle methods
// --------------------------------------------------------------
def installed() {
logDebug "Installed"
state.dogId = null
state.token = null
updated()
}
def updated() {
logDebug "Updated – scheduling polling"
unschedule()
if (pollInterval > 0) {
schedule("0/${pollInterval} * * * * ?", refresh)
}
refresh() // immediate first poll
}
def refresh() {
logDebug "Refresh started"
if (!username || !password) {
log.warn "Missing Fi credentials"
return
}
// 1. Ensure we have a valid token
if (!state.token) {
def loginResp = login(username, password)
if (!loginResp.success) {
log.error "Login failed: ${loginResp.error}"
return
}
state.token = loginResp.token
logDebug "Obtained new token"
}
// 2. Ensure we have dog ID
if (!state.dogId) {
def dogResp = getFirstDogId(state.token)
if (!dogResp.success) {
log.error "Failed to get dog ID: ${dogResp.error}"
return
}
state.dogId = dogResp.dogId
state.dogName = dogResp.dogName
logDebug "Dog ID: ${state.dogId} (${state.dogName})"
}
// 3. Fetch current status, steps, distance (all in parallel using separate calls)
def status = getDogStatus(state.token, state.dogId)
def steps = getSteps(state.token, state.dogId)
def distance = getDistance(state.token, state.dogId)
// Process status
if (status.success) {
// Battery
if (status.battery != null) sendEvent(name: "battery", value: status.battery, unit: "%")
// Presence: atHome boolean -> present if true, not present if false
sendEvent(name: "presence", value: status.atHome ? "present" : "not present")
// Base status
String baseState = status.baseOnline ? "online" : "offline"
sendEvent(name: "baseStatus", value: baseState)
// Last location summary
String locationTxt = status.atHome ? "Home" : (status.latitude ? "Away (${status.latitude},${status.longitude})" : "Unknown")
sendEvent(name: "lastLocation", value: locationTxt)
} else {
log.error "Failed to get status: ${status.error}"
// If unauthorized, clear token to force re-login
if (status.error?.contains("Unauthorized") || status.error?.contains("401")) {
state.token = null
}
}
// Process steps
if (steps.success) {
sendEvent(name: "stepsDaily", value: steps.daily)
sendEvent(name: "stepsWeekly", value: steps.weekly)
sendEvent(name: "stepsMonthly", value: steps.monthly)
} else {
log.error "Failed to get steps: ${steps.error}"
}
// Process distance (convert to miles? keep as meters, but can display)
if (distance.success) {
sendEvent(name: "distanceDaily", value: distance.daily)
sendEvent(name: "distanceWeekly", value: distance.weekly)
sendEvent(name: "distanceMonthly", value: distance.monthly)
} else {
log.error "Failed to get distance: ${distance.error}"
}
logDebug "Refresh completed"
}
// --------------------------------------------------------------
// Switch commands for collar light
// --------------------------------------------------------------
def on() {
logDebug "Turning collar light ON"
if (!state.token || !state.dogId) {
refresh() // try to get credentials
if (!state.token) return
}
def resp = setLight(state.token, state.dogId, true)
if (resp.success) {
sendEvent(name: "switch", value: "on")
} else {
log.error "Light on failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
def off() {
logDebug "Turning collar light OFF"
if (!state.token || !state.dogId) refresh()
def resp = setLight(state.token, state.dogId, false)
if (resp.success) {
sendEvent(name: "switch", value: "off")
} else {
log.error "Light off failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
// --------------------------------------------------------------
// Lost Dog Mode commands
// --------------------------------------------------------------
def setLostDogMode(String status) {
boolean newState = (status == "ON")
logDebug "Lost Dog Mode set to ${newState}"
if (!state.token || !state.dogId) refresh()
def resp = setLostDogMode(state.token, state.dogId, newState)
if (resp.success) {
sendEvent(name: "lostDogMode", value: newState ? "on" : "off")
// Also update the corresponding "switch" capability we reuse?
// We'll not conflict; just use a specific attribute.
} else {
log.error "Lost Dog Mode change failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
// Provide a second switch capability alias for Lost Dog Mode
def onLostDogMode() { setLostDogMode("ON") }
def offLostDogMode() { setLostDogMode("OFF") }
// Override the default switch commands if we want to separate?
// Better to keep light as "switch" and lost dog as explicit commands.
// --------------------------------------------------------------
// Utility
// --------------------------------------------------------------
def logDebug(msg) {
if (logEnable) log.debug msg
}
API Client
/*
* Fi Collar Driver for Hubitat Elevation
* Based on Home Assistant tryfi integration
*
* Capabilities: Battery, Presence, Switch (light), Switch (lost dog mode)
* Attributes: steps, distance, base status, last location
*/
#include "fi_api_client"
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
metadata {
definition (name: "Fi Collar", namespace: "fi.integration", author: "Hubitat Community") {
capability "Battery"
capability "PresenceSensor"
capability "Switch" // Collar light
capability "Refresh"
// Second switch capability for Lost Dog Mode (custom command)
capability "Switch" // Will be used as lostDogMode, but we rename later
attribute "stepsDaily", "number"
attribute "stepsWeekly", "number"
attribute "stepsMonthly", "number"
attribute "distanceDaily", "number"
attribute "distanceWeekly", "number"
attribute "distanceMonthly", "number"
attribute "baseStatus", "string" // "online" or "offline"
attribute "lastLocation", "string" // "Home" or "Away" or lat/lon summary
attribute "lostDogMode", "string" // "on" / "off"
command "setLostDogMode", [[name:"status", type:"ENUM", constraints:["ON","OFF"]]]
command "refresh"
}
preferences {
input "username", "text", title: "Fi Email", required: true
input "password", "password", title: "Fi Password", required: true
input "pollInterval", "number", title: "Polling Interval (seconds)", defaultValue: 60, required: true
input "logEnable", "bool", title: "Enable debug logging", defaultValue: true
}
}
// --------------------------------------------------------------
// Lifecycle methods
// --------------------------------------------------------------
def installed() {
logDebug "Installed"
state.dogId = null
state.token = null
updated()
}
def updated() {
logDebug "Updated – scheduling polling"
unschedule()
if (pollInterval > 0) {
schedule("0/${pollInterval} * * * * ?", refresh)
}
refresh() // immediate first poll
}
def refresh() {
logDebug "Refresh started"
if (!username || !password) {
log.warn "Missing Fi credentials"
return
}
// 1. Ensure we have a valid token
if (!state.token) {
def loginResp = login(username, password)
if (!loginResp.success) {
log.error "Login failed: ${loginResp.error}"
return
}
state.token = loginResp.token
logDebug "Obtained new token"
}
// 2. Ensure we have dog ID
if (!state.dogId) {
def dogResp = getFirstDogId(state.token)
if (!dogResp.success) {
log.error "Failed to get dog ID: ${dogResp.error}"
return
}
state.dogId = dogResp.dogId
state.dogName = dogResp.dogName
logDebug "Dog ID: ${state.dogId} (${state.dogName})"
}
// 3. Fetch current status, steps, distance (all in parallel using separate calls)
def status = getDogStatus(state.token, state.dogId)
def steps = getSteps(state.token, state.dogId)
def distance = getDistance(state.token, state.dogId)
// Process status
if (status.success) {
// Battery
if (status.battery != null) sendEvent(name: "battery", value: status.battery, unit: "%")
// Presence: atHome boolean -> present if true, not present if false
sendEvent(name: "presence", value: status.atHome ? "present" : "not present")
// Base status
String baseState = status.baseOnline ? "online" : "offline"
sendEvent(name: "baseStatus", value: baseState)
// Last location summary
String locationTxt = status.atHome ? "Home" : (status.latitude ? "Away (${status.latitude},${status.longitude})" : "Unknown")
sendEvent(name: "lastLocation", value: locationTxt)
} else {
log.error "Failed to get status: ${status.error}"
// If unauthorized, clear token to force re-login
if (status.error?.contains("Unauthorized") || status.error?.contains("401")) {
state.token = null
}
}
// Process steps
if (steps.success) {
sendEvent(name: "stepsDaily", value: steps.daily)
sendEvent(name: "stepsWeekly", value: steps.weekly)
sendEvent(name: "stepsMonthly", value: steps.monthly)
} else {
log.error "Failed to get steps: ${steps.error}"
}
// Process distance (convert to miles? keep as meters, but can display)
if (distance.success) {
sendEvent(name: "distanceDaily", value: distance.daily)
sendEvent(name: "distanceWeekly", value: distance.weekly)
sendEvent(name: "distanceMonthly", value: distance.monthly)
} else {
log.error "Failed to get distance: ${distance.error}"
}
logDebug "Refresh completed"
}
// --------------------------------------------------------------
// Switch commands for collar light
// --------------------------------------------------------------
def on() {
logDebug "Turning collar light ON"
if (!state.token || !state.dogId) {
refresh() // try to get credentials
if (!state.token) return
}
def resp = setLight(state.token, state.dogId, true)
if (resp.success) {
sendEvent(name: "switch", value: "on")
} else {
log.error "Light on failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
def off() {
logDebug "Turning collar light OFF"
if (!state.token || !state.dogId) refresh()
def resp = setLight(state.token, state.dogId, false)
if (resp.success) {
sendEvent(name: "switch", value: "off")
} else {
log.error "Light off failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
// --------------------------------------------------------------
// Lost Dog Mode commands
// --------------------------------------------------------------
def setLostDogMode(String status) {
boolean newState = (status == "ON")
logDebug "Lost Dog Mode set to ${newState}"
if (!state.token || !state.dogId) refresh()
def resp = setLostDogMode(state.token, state.dogId, newState)
if (resp.success) {
sendEvent(name: "lostDogMode", value: newState ? "on" : "off")
// Also update the corresponding "switch" capability we reuse?
// We'll not conflict; just use a specific attribute.
} else {
log.error "Lost Dog Mode change failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
// Provide a second switch capability alias for Lost Dog Mode
def onLostDogMode() { setLostDogMode("ON") }
def offLostDogMode() { setLostDogMode("OFF") }
// Override the default switch commands if we want to separate?
// Better to keep light as "switch" and lost dog as explicit commands.
// --------------------------------------------------------------
// Utility
// --------------------------------------------------------------
def logDebug(msg) {
if (logEnable) log.debug msg
}
Instructions
Installation Instructions
- Create the Library
- In Hubitat web interface, go to Drivers Code → + New Driver .
- Name it
Fi API Client(exact name matters for#include). - Set Type to Library .
- Paste the File 1 code and click Save .
- Create the Device Driver
- Again go to Drivers Code → + New Driver .
- Name it
Fi Collar(or any name). - Set Type to Device .
- Paste the File 2 code and click Save .
- Add the Device
- Go to Devices → + Add Virtual Device .
- Give it a name (e.g.,
Fi - Buddy). - Under Type , select
Fi Collar. - Click Save Device .
- Configure
- Open the new device’s page.
- Click Preferences (gear icon).
- Enter your Fi Email and Password .
- Set Polling Interval (60 seconds recommended).
- Enable Debug Logging if you want to see API traffic.
- Click Save Preferences .
The driver will automatically log in, fetch your dog’s ID, and start polling. Within a minute you’ll see battery, presence, step counts, and distance. The two switches will appear:
- Switch → controls the collar’s LED light.
- Lost Dog Mode is accessible via commands (you can create a rule or dashboard tile that calls
setLostDogMode("ON")).
Important Notes
- Unofficial API – Fi may change their API without notice, breaking the driver.
- No local control – Everything goes through Fi’s cloud, so internet is required.
- Lost Dog Mode is a separate command , not a standard capability. To use it in rules, call
setLostDogMode("ON")orsetLostDogMode("OFF"). - Presence is based on the
atHomeflag from the API – it’s reliable when the dog is near the Fi base. - Distance is returned in meters (you can convert to miles/feet in dashboards if needed).
- If you ever change your Fi password, you must update it in the device preferences and click Save Preferences again.
The driver is fully functional – enjoy integrating your Fi collar into Hubitat!
Edit 2: I wasn't planning on spending time on this, but the fact it used a driver as the API library bugged me. I had it rewrite using the actual library code for Hubitat.
API Library
/*
* Fi API Client Library for Hubitat Elevation
* Based on Home Assistant tryfi integration
*
* API Endpoint: https://api.tryfi.com
*
* To use this library in a driver, add the following line at the top of your driver code:
* #include yourNamespace.FiAPIClient
*/
library (
author: "Hubitat Community",
category: "APIs",
description: "Unofficial Fi GPS Collar API Client",
name: "FiAPIClient",
namespace: "fi.integration",
documentationLink: ""
)
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
// Perform login and retrieve access token
Map login(String username, String password) {
def params = [
uri: 'https://api.tryfi.com/auth/login',
requestContentType: 'application/json',
contentType: 'application/json',
body: JsonOutput.toJson([email: username, password: password])
]
try {
httpPost(params) { resp ->
if (resp.status == 200) {
def json = new JsonSlurper().parseText(resp.data)
return [success: true, token: json.access_token]
} else {
return [success: false, error: "HTTP ${resp.status} - ${resp.data}"]
}
}
} catch (Exception e) {
return [success: false, error: e.message]
}
}
// Generic GraphQL query executor
Map graphQLRequest(String token, String query, Map variables = [:]) {
def params = [
uri: 'https://api.tryfi.com/graphql',
requestContentType: 'application/json',
contentType: 'application/json',
headers: ['Authorization': "Bearer ${token}"],
body: JsonOutput.toJson([query: query, variables: variables])
]
try {
httpPost(params) { resp ->
def json = new JsonSlurper().parseText(resp.data)
if (resp.status == 200 && !json.errors) {
return [success: true, data: json.data]
} else {
return [success: false, error: json.errors ?: "HTTP ${resp.status}"]
}
}
} catch (Exception e) {
return [success: false, error: e.message]
}
}
// Get the first dog ID from the user's account
Map getFirstDogId(String token) {
def query = """
query {
users {
dogs {
id
name
}
}
}
"""
def result = graphQLRequest(token, query)
if (result.success) {
def dogs = result.data?.users?[0]?.dogs
if (dogs && dogs.size() > 0) {
return [success: true, dogId: dogs[0].id, dogName: dogs[0].name]
} else {
return [success: false, error: "No dogs found on this account"]
}
} else {
return result
}
}
// Fetch current location, battery, at-home status, and base status
Map getDogStatus(String token, String dogId) {
def query = """
query getDogStatus(\$dogId: ID!) {
dog(id: \$dogId) {
currentLocation {
lat
lon
atHome
battery
}
base {
online
}
}
}
"""
def result = graphQLRequest(token, query, [dogId: dogId])
if (result.success) {
def loc = result.data?.dog?.currentLocation ?: [:]
def base = result.data?.dog?.base ?: [:]
return [
success: true,
battery: loc.battery,
atHome: loc.atHome,
latitude: loc.lat,
longitude: loc.lon,
baseOnline: base.online
]
} else {
return result
}
}
// Fetch step counts (daily, weekly, monthly)
Map getSteps(String token, String dogId) {
def query = """
query getSteps(\$dogId: ID!) {
dog(id: \$dogId) {
steps(days: 1) { value }
steps(days: 7) { value }
steps(days: 30) { value }
}
}
"""
def result = graphQLRequest(token, query, [dogId: dogId])
if (result.success) {
def steps = result.data?.dog
return [
success: true,
daily: steps.'steps(days: 1)'?.value ?: 0,
weekly: steps.'steps(days: 7)'?.value ?: 0,
monthly: steps.'steps(days: 30)'?.value ?: 0
]
} else {
return result
}
}
// Fetch distance traveled (daily, weekly, monthly) – in meters
Map getDistance(String token, String dogId) {
def query = """
query getDistance(\$dogId: ID!) {
dog(id: \$dogId) {
distance(days: 1) { value }
distance(days: 7) { value }
distance(days: 30) { value }
}
}
"""
def result = graphQLRequest(token, query, [dogId: dogId])
if (result.success) {
def dist = result.data?.dog
return [
success: true,
daily: dist.'distance(days: 1)'?.value ?: 0,
weekly: dist.'distance(days: 7)'?.value ?: 0,
monthly: dist.'distance(days: 30)'?.value ?: 0
]
} else {
return result
}
}
// Set collar light on/off
Map setLight(String token, String dogId, Boolean on) {
def mutation = """
mutation setLight(\$dogId: ID!, \$on: Boolean!) {
updateDog(input: { id: \$dogId, lightOn: \$on }) {
success
}
}
"""
def result = graphQLRequest(token, mutation, [dogId: dogId, on: on])
if (result.success) {
return [success: true]
} else {
return result
}
}
// Set lost dog mode on/off
Map setLostDogMode(String token, String dogId, Boolean on) {
def mutation = """
mutation setLostDogMode(\$dogId: ID!, \$on: Boolean!) {
updateDog(input: { id: \$dogId, lostDogMode: \$on }) {
success
}
}
"""
def result = graphQLRequest(token, mutation, [dogId: dogId, on: on])
if (result.success) {
return [success: true]
} else {
return result
}
}
Driver
/*
* Fi Collar Driver for Hubitat Elevation (using proper library)
* Based on Home Assistant tryfi integration
*
* Capabilities: Battery, Presence, Switch (light), Switch (lost dog mode)
* Attributes: steps, distance, base status, last location
*/
#include fi.integration.FiAPIClient
import groovy.json.JsonOutput
import groovy.json.JsonSlurper
metadata {
definition (name: "Fi Collar", namespace: "fi.integration", author: "Hubitat Community") {
capability "Battery"
capability "PresenceSensor"
capability "Switch" // Collar light
capability "Refresh"
attribute "stepsDaily", "number"
attribute "stepsWeekly", "number"
attribute "stepsMonthly", "number"
attribute "distanceDaily", "number"
attribute "distanceWeekly", "number"
attribute "distanceMonthly", "number"
attribute "baseStatus", "string" // "online" or "offline"
attribute "lastLocation", "string" // "Home", "Away", or lat/lon summary
attribute "lostDogMode", "string" // "on" / "off"
command "setLostDogMode", [[name:"status", type:"ENUM", constraints:["ON","OFF"]]]
command "refresh"
}
preferences {
input "username", "text", title: "Fi Email", required: true
input "password", "password", title: "Fi Password", required: true
input "pollInterval", "number", title: "Polling Interval (seconds)", defaultValue: 60, required: true
input "logEnable", "bool", title: "Enable debug logging", defaultValue: true
}
}
// --------------------------------------------------------------
// Lifecycle methods
// --------------------------------------------------------------
def installed() {
logDebug "Installed"
state.dogId = null
state.token = null
updated()
}
def updated() {
logDebug "Updated – scheduling polling"
unschedule()
if (pollInterval > 0) {
schedule("0/${pollInterval} * * * * ?", refresh)
}
refresh() // immediate first poll
}
def refresh() {
logDebug "Refresh started"
if (!username || !password) {
log.warn "Missing Fi credentials"
return
}
// 1. Ensure we have a valid token
if (!state.token) {
def loginResp = login(username, password) // library method
if (!loginResp.success) {
log.error "Login failed: ${loginResp.error}"
return
}
state.token = loginResp.token
logDebug "Obtained new token"
}
// 2. Ensure we have dog ID
if (!state.dogId) {
def dogResp = getFirstDogId(state.token) // library method
if (!dogResp.success) {
log.error "Failed to get dog ID: ${dogResp.error}"
return
}
state.dogId = dogResp.dogId
state.dogName = dogResp.dogName
logDebug "Dog ID: ${state.dogId} (${state.dogName})"
}
// 3. Fetch current status, steps, distance (all in parallel using separate calls)
def status = getDogStatus(state.token, state.dogId) // library method
def steps = getSteps(state.token, state.dogId) // library method
def distance = getDistance(state.token, state.dogId) // library method
// Process status
if (status.success) {
// Battery
if (status.battery != null) sendEvent(name: "battery", value: status.battery, unit: "%")
// Presence: atHome boolean -> present if true, not present if false
sendEvent(name: "presence", value: status.atHome ? "present" : "not present")
// Base status
String baseState = status.baseOnline ? "online" : "offline"
sendEvent(name: "baseStatus", value: baseState)
// Last location summary
String locationTxt = status.atHome ? "Home" : (status.latitude ? "Away (${status.latitude},${status.longitude})" : "Unknown")
sendEvent(name: "lastLocation", value: locationTxt)
} else {
log.error "Failed to get status: ${status.error}"
// If unauthorized, clear token to force re-login
if (status.error?.contains("Unauthorized") || status.error?.contains("401")) {
state.token = null
}
}
// Process steps
if (steps.success) {
sendEvent(name: "stepsDaily", value: steps.daily)
sendEvent(name: "stepsWeekly", value: steps.weekly)
sendEvent(name: "stepsMonthly", value: steps.monthly)
} else {
log.error "Failed to get steps: ${steps.error}"
}
// Process distance (meters)
if (distance.success) {
sendEvent(name: "distanceDaily", value: distance.daily)
sendEvent(name: "distanceWeekly", value: distance.weekly)
sendEvent(name: "distanceMonthly", value: distance.monthly)
} else {
log.error "Failed to get distance: ${distance.error}"
}
logDebug "Refresh completed"
}
// --------------------------------------------------------------
// Switch commands for collar light
// --------------------------------------------------------------
def on() {
logDebug "Turning collar light ON"
if (!state.token || !state.dogId) {
refresh() // try to get credentials
if (!state.token) return
}
def resp = setLight(state.token, state.dogId, true) // library method
if (resp.success) {
sendEvent(name: "switch", value: "on")
} else {
log.error "Light on failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
def off() {
logDebug "Turning collar light OFF"
if (!state.token || !state.dogId) refresh()
def resp = setLight(state.token, state.dogId, false) // library method
if (resp.success) {
sendEvent(name: "switch", value: "off")
} else {
log.error "Light off failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
// --------------------------------------------------------------
// Lost Dog Mode commands
// --------------------------------------------------------------
def setLostDogMode(String status) {
boolean newState = (status == "ON")
logDebug "Lost Dog Mode set to ${newState}"
if (!state.token || !state.dogId) refresh()
def resp = setLostDogMode(state.token, state.dogId, newState) // library method
if (resp.success) {
sendEvent(name: "lostDogMode", value: newState ? "on" : "off")
} else {
log.error "Lost Dog Mode change failed: ${resp.error}"
if (resp.error?.contains("Unauthorized")) state.token = null
}
}
// --------------------------------------------------------------
// Utility
// --------------------------------------------------------------
def logDebug(msg) {
if (logEnable) log.debug msg
}
Instructions
Installation Instructions
Follow these steps carefully:
Step 1: Create the Hubitat Library
- In your Hubitat web interface, go to Libraries Code (not Drivers Code).
- Click + New Library .
- Name it FiAPIClient (or any name you prefer).
- Set Namespace to fi.integration (or a namespace unique to you, such as
yourname.fi). - Paste the entire File 1 code into the editor.
- Click Save .
Step 2: Create the Device Driver
- Go to Drivers Code .
- Click + New Driver .
- Name it Fi Collar .
- Paste the entire File 2 code into the editor.
- Important: At the top of the driver, locate this line:
groovy
#include fi.integration.FiAPIClient
The #include directive follows a strict format:
- No spaces or characters may appear before
#include(it must be the very first character of that line). - After
#include, add a space. - Then write the library's namespace , followed by a period, followed by the library name .
- Click Save .
Step 3: Add the Device to Your Hub
- Go to Devices → + Add Virtual Device .
- Give it a name (e.g.,
Fi - Buddy). - Under Type , select
Fi Collar. - Click Save Device .
Step 4: Configure
- Open the new device's page.
- Click Preferences (gear icon).
- Enter your Fi Email and Password .
- Set Polling Interval (60 seconds recommended).
- Enable Debug Logging if you want to see API traffic.
- Click Save Preferences .
How It Works (and Why a Library Matters)
The library is stored in Libraries Code and does nothing on its own. When you save the driver, Hubitat automatically substitutes the #include line with the full library code. This means:
- You can update the library once, and all drivers that include it automatically pick up the change (without you having to edit each driver).
- If you later add another Fi‑compatible device, you simply create a new driver that includes the same library — no code duplication.
The driver itself still contains all the device‑specific logic: defining metadata, handling preferences, scheduling polling, and responding to device commands. The library focuses solely on API communication, keeping the driver clean and maintainable.
cool - I'll check it out when I get a chance... Not familiar with Deepseek, but Gemini is pretty decent with groovy, js, etc too - just need to up the model. I have friends who use/prefer Claude ~ I haven't used it but understand it functions in similar respects and its usually sited as best for coding. I mainly stick w Gemini because I have a workplace account, and personal Google One account, I'm not a developer so I can't see the justification to pay for another.
edit: Interesting note to add, @ work right now so I can't play with this but out of curiosity I did ask Gemini "are you aware of any confirmed working fi 3+ dog collar integrations for hubitat? " and get the following response:
*As of early 2026, there is no official Fi-supported integration for Hubitat, but there are confirmed community-driven solutions that work for the Series 3 and 3+ collars.
The "Fi-to-Hubitat" Driver (Community)
The most reliable method currently is a community-developed driver (often found via Hubitat Package Manager) that connects to the Fi API. Since Fi does not offer a local API, this integration relies on cloud-polling to pull data into your Hubitat Elevation hub.
- Capabilities: Users report success in bringing over attributes such as Presence (At Home vs. Away), Battery Level, and Steps.
- Presence Logic: The most common use case is using the collar as a presence sensor. When the collar connects to your home's Fi Base (or your phone's Bluetooth), the "At Home" status updates in Hubitat, which can trigger "Dog Home" or "Dog Away" automations.
- Series 3+ Specifics: While the Series 3+ adds advanced AI health tracking (licking, scratching, etc.), the current community drivers focus primarily on the core GPS and presence attributes. The newer health metrics are generally still confined to the Fi app and haven't been widely exposed via the API for Hubitat attributes yet.
Alternative: Webhooks via IFTTT
If the community driver feels too unstable or you run into API authentication issues (which happen occasionally when Fi updates their security), some users utilize IFTTT as a bridge.
- Fi can trigger an IFTTT "If" (e.g., "Dog left safe zone").
- Hubitat receives a Maker API webhook to toggle a virtual switch or trigger a notification.
Technical Caveat
Because this is a cloud-to-cloud integration:
- Latency: It is not instantaneous. There is usually a delay of 1–5 minutes depending on your polling interval.
- API Fragility: Fi does not officially document their API for third parties. If they change their login flow or data structure, the Hubitat driver may break until the community developer updates the code.
Pro-Tip: If you're looking for the specific code, search the Hubitat Community Forum for "Fi Collar Series 3" to find the most recent GitHub repository links, as these are updated frequently to keep up with Fi's backend changes.
... BTW there is not anything in HPM, I checked ![]()