Starlink - Anyone using info in Hubitat (for dashboard etc)?

I finally received my Starlink kit yesterday after 3 weeks and got it set up. While browsing a Facebook forum I noticed someone mentioned they'd pulled data into Home Assistant for display on a dashboard. I wondered if anyone has done similar for Hubitat and how? I've always had a little Broadband Quality Monitor running in the information page of my dashboards, but as Starlink is CGNAT that will not work anymore (relies on incoming ping). It would be good to replace that with something relevant to Starlink. I'll have a chat with AI in the meantime to see what's doable.

not doable unless u set up a full time vpn on a router on your starlink network

I've already setup Tailscale on my Pi so I can access behind CGNAT of Starlink.

I'm at a standstill until I put my Starlink in bypass and replace the connection from existing ISPs modem into Unifi UDR. It does look doable though. I need to install something on my Pi to poll the Starlink dish of the data, then it'll use node-red and maker to populate the attributes of a driver (also possible with mqtt).

I'll post when I have something working (famous last words!).....

what info are u trying to get. I have a hubitat in a different house continually pinging out cottage starlink every 15 minutes and reporting if it is down. I also have my Asus router run speed tests (openspeedtest) every 4 hours and email me the results. other than that all I can see in the app is average ping times and watts used. if u manage to get that info somehow let me know

The screenshot below shows what someone did for HA. I believe there may be more information available than that though, for instance obstruction data is available:

A better example (though I'll just want a few values for tiles):

i looked it up looks like the data lives on 192.168.100.1 port 9201.. i can ping that ip when ssh into my asus router behind the starlink router in passthru mode.. but my hubitat is not local to that location nor do i have anything full time that can run a docker image.. oh well. seems like to much work when i can just go into the app and look at the data.

this is my last speedtest run on the router... its been working well for me for about 3 years even with 370 inches of snow this year. Not happy about the 10 price increase though.. may drop the speed this winter when cottage is empty..

The price increase has wound a lot of people up. I signed up to a deal then they put the price up before my kit arrived. Thankfully I'm locked in at the agreed price for at least 6 months.

My situation is a little different in that I can get fast cable internet where I am but no FTTC, FTTP or 5G services. I decided to tell my cable provider to stick it after hiking prices so I'm just going with Starlink on the lower package for now but may have to go back to cable later.

So I made a start. Didn't need to use node-red or mqtt. There's just a file in Python on my Pi getting the information from the Starlink dish. That updates a driver in Hubitat every 30 seconds with the various attributes using maker api. So far I have the following:

Next steps:

  1. add the azimuth/elevation values for the dish (better might be calculated difference between desired and actual)
  2. I'd have liked power and total download/upload values but I guess I'm not seeing them anywhere due to my Starlink router being in Bypass mode
  3. rename drop rate to packet loss
  4. convert numbers to strings to include appropriate unit for dashboard display (mS, Mb/s etc)
  5. add a command to driver and script to allow a reboot of Starlink to be called
  6. add descriptionText logging to the driver

I have been pulling info via the info page:

http://192.168.100.1 which shows when my dish is in router bypass. It's the same location where Unifi's UDM's also pull stats for their dashboard:

image

Yeah I can get all of the information at that page and in the app. I wanted a way to get the values into attributes that could be displayed on dashboard tiles. I don't particularly need to but it's handy.

/**
 * Starlink Dish Status Driver for Hubitat
 * Version: 1.0
 */

metadata {
    definition (name: "Starlink Dish Status", namespace: "RonV42", author: "AI assisted") {
        capability "Refresh"
        capability "Sensor"
        
        // Main attributes
        attribute "status", "string"
        attribute "alignmentStatus", "string"
        attribute "tiltRecommendation", "string"
        attribute "rotateRecommendation", "string"
        attribute "currentTilt", "number"
        attribute "currentRotation", "number"
        attribute "targetTilt", "number"
        attribute "targetRotation", "number"
        attribute "softwareVersion", "string"
        attribute "hardwareVersion", "string"
        attribute "stowed", "string"
        attribute "alertSummary", "string"
        
        // Individual alerts
        attribute "dishHeating", "string"
        attribute "thermalShutdown", "string"
        attribute "motorsHealthy", "string"
        attribute "obstructed", "string"
        attribute "mastNearVertical", "string"
    }

    preferences {
        input name: "ipAddress", type: "text", title: "Starlink IP Address", required: true, defaultValue: "192.168.100.1"
        input name: "pollInterval", type: "number", title: "Poll Interval (minutes)", required: true, defaultValue: 5
    }
}

def installed() {
    log.info "Starlink Dish driver installed"
    initialize()
}

def updated() {
    log.info "Starlink Dish driver updated"
    unschedule()
    initialize()
}

def initialize() {
    schedule("0 */${pollInterval} * * * ?", "refresh")
    refresh()
}

def refresh() {
    def ip = settings.ipAddress ?: "192.168.100.1"
    def url = "http://${ip}"
    
    try {
        httpGet([uri: url, timeout: 10]) { response ->
            if (response.status == 200) {
                parsePage(response.data)
            } else {
                log.error "Failed to connect to Starlink: ${response.status}"
                sendEvent(name: "status", value: "Offline")
            }
        }
    } catch (e) {
        log.error "Error fetching Starlink data: ${e}"
        sendEvent(name: "status", value: "Error")
    }
}

private void parsePage(html) {
    def text = html.toString()
    
    // Try to extract the diagnostic JSON (it's embedded in the page)
    def jsonMatcher = text =~ /(?s)\{.*?"id":.*?"stowed":.*?\}/
    def jsonStr = jsonMatcher.find() ? jsonMatcher.group(0) : null
    
    def data = null
    if (jsonStr) {
        try {
            data = new groovy.json.JsonSlurper().parseText(jsonStr)
        } catch (e) {
            log.warn "Failed to parse diagnostic JSON"
        }
    }
    
    if (!data) {
        log.warn "Could not find diagnostic data"
        return
    }
    
    // Update attributes
    sendEvent(name: "softwareVersion", value: data.softwareVersion ?: "Unknown")
    sendEvent(name: "hardwareVersion", value: data.hardwareVersion ?: "Unknown")
    sendEvent(name: "stowed", value: data.stowed ? "Yes" : "No")
    
    // Alignment
    def align = data.alignmentStats ?: [:]
    sendEvent(name: "currentTilt", value: align.boresightElevationDeg?.toFloat() ?: 0)
    sendEvent(name: "currentRotation", value: align.boresightAzimuthDeg?.toFloat() ?: 0)
    sendEvent(name: "targetTilt", value: align.desiredBoresightElevationDeg?.toFloat() ?: 0)
    sendEvent(name: "targetRotation", value: align.desiredBoresightAzimuthDeg?.toFloat() ?: 0)
    
    // Parse alignment status from visible text (approximate)
    def alignmentText = text.contains("Okay") ? "Okay" : "Misaligned"
    sendEvent(name: "alignmentStatus", value: alignmentText)
    
    // Parse recommendations (rough regex)
    def tiltRec = (text =~ /Tilt recommendation.*?([\d.]+)°/).findAll()
    def rotateRec = (text =~ /Rotate recommendation.*?([\d.]+)°/).findAll()
    
    sendEvent(name: "tiltRecommendation", value: tiltRec ? "${tiltRec[0][1]}°" : "0°")
    sendEvent(name: "rotateRecommendation", value: rotateRec ? "${rotateRec[0][1]}°" : "0°")
    
    // Alerts
    def alerts = data.alerts ?: [:]
    sendEvent(name: "dishHeating", value: alerts.dishHeating ? "Heating" : "Normal")
    sendEvent(name: "thermalShutdown", value: alerts.dishThermalShutdown ? "Shutdown" : "Normal")
    sendEvent(name: "motorsHealthy", value: alerts.motorsStuck ? "Issue" : "Healthy")
    sendEvent(name: "obstructed", value: alerts.obstructed ? "Yes" : "No")
    sendEvent(name: "mastNearVertical", value: alerts.mastNotNearVertical ? "No" : "Yes")
    
    // Overall status
    def hasIssues = alerts.values().any { it == true }
    sendEvent(name: "status", value: hasIssues ? "Warning" : "Online")
    sendEvent(name: "alertSummary", value: hasIssues ? "Active Alerts" : "All Clear")
    
    log.info "Starlink data refreshed successfully"
}

@ronv42 - is that just working directly? Hubitat > Starlink at 192.168.100.1?

If so, I've wasted an evening....

Fozzie Bear Reaction GIF

I have to open my firewall up to the hubitat to do more testing. It worked from my PC network now I just have to test on the IoT network where firewall rules blocked the route.

Darn I would hate to move the entire vlan to trusted zone just to test so far my UDM pro is still blocking my Hubitat from reaching a that network address. I'll keep trying later today.

Let me know if it works for you.

I just get a 'could not find diagnostic data' error unfortunately. Everything on one flat network here - no VLANS, so the 192.168.100.1 page is available (Starlink connected to my Unifi)

I get connection timed out due to my firewall. Well the framework is there.

Yep I think I found the issue in the RegEx. I hate RegEx. I think I need to re-work the parser section give me about a hour or so

Have you had it working previously? Only reason I used the Pi was that AI said I needed 'starlink-grpc-tools' to access the data

Yes had it working previously. Very very rough code. I am going to change to using the "div" filter in the HTML to target the JSON vs. RegEx. Much more reliable. My issue is that I have been doing security updates to my home network. Tightening up horizonal traversal and no IoT devices a blocked from reaching out to other "local" networks.

Just had a look on ChatGPT with your driver. What it is saying is that changes in the starlink firmware mean that the JSON is not available as it used to be as it's no longer embedded in the html. Probably why it suggested the Python helper which uses the local gRPC API on port 9200. All over my head, but what I've cobbled together is working well. I just need to add and tweak a few things. :+1:

It's possible but my dump of the HTML shows a "div" with the fully embedded JSON payload.