Lighting

Hubspace: Following the Breadcrumbs

Reverse-engineering an undocumented platform without pretending guesses are facts

reverse engineeringhome automationhomeseerhubspaceaferocloud apiautomationnetworksoftwareoauthpkcesoftware-automation

Hubspace: Following the Breadcrumbs

Not in the HomeSeer store quite yet. It’s been running here every day, and the plan is to send it to the store as a beta.

Hubspace is The Home Depot’s smart-home platform, powered underneath by Afero.

It sits behind an increasingly large collection of inexpensive lights, fans, switches, outlets, locks, and other devices sold under several Home Depot brands.

There is no public Hubspace developer API.

No integration guide.

No friendly document explaining:

“Here is how to talk to your devices.”

There is, however, an app.

And an app has to talk to something.

So this one began the same way the Sleep Number integration did:

Watch what the real client actually does.

Write it down.

Then build against the evidence rather than against assumptions.

Map first, code second

Before writing the plugin itself, I spent time capturing the requests made by the actual Hubspace client and documenting what came back.

Read-only first.

Not:

“This endpoint probably does this.”

Not:

“Someone on a forum said this worked a few years ago.”

But:

This request went here, with these headers, and this was the response.

That record became the baseline for the integration.

Anything directly observed is treated as observed.

Anything inferred is labeled as inference.

Anything not verified is not quietly promoted into fact just because it would make the code comment shorter.

That discipline matters more with an undocumented API than almost anywhere else.

If something breaks later, I want the question to be:

“What changed?”

not:

“Did I misunderstand this from the beginning?”

Three services, one of them hiding in plain sight

The traffic I captured revealed three distinct pieces involved in making the Hubspace app work.

There is the primary Afero API used for device and account activity.

There is a separate service providing semantics and metadata.

And authentication goes through a Keycloak identity service.

The Keycloak URL itself contains a mildly amusing breadcrumb.

The realm is named:

thd

It does not take much detective work to guess what that stands for.

Undocumented systems have a habit of revealing bits of their organizational history in host names, realm names, headers, and other places nobody expected users to read.

The metadata service was the more interesting discovery.

The endpoint that “doesn’t exist”

In the client behavior I captured, the metadata backend was reached through the same apparent network address as the primary API.

The distinction was made using the HTTP Host header.

Send the request with the expected Host value and it reaches the metadata service.

Send what otherwise appears to be the same request without that override and the server returns:

404

Which is a wonderfully misleading answer.

A 404 normally means:

“That endpoint does not exist.”

In this case, what I had really done was ask the wrong backend for an endpoint that existed perfectly well somewhere else behind the same infrastructure.

Those are very different debugging problems.

And nothing in:

404 Not Found

offers to explain the distinction.

This is exactly why capturing the real client matters.

If you begin by guessing at endpoints, you conclude the resource is gone.

If you begin by asking:

“What does the working app actually send?”

the missing Host header becomes visible.

Signing in without keeping the password

The current authentication flow I implemented follows the same general modern pattern used by the Hubspace client:

OAuth 2.0 Authorization Code with PKCE, backed by Keycloak.

Ordinarily, that is a browser-oriented process.

The application sends the user to the identity provider.

The user signs in.

The identity provider redirects the browser back with an authorization code.

The application exchanges that code for tokens.

Except a HomeSeer plugin does not have a convenient interactive browser sitting inside it waiting to complete the dance.

So the plugin performs the necessary browser-style portion of the flow itself.

It fetches the login page.

Reads the form destination it is given.

Submits the credentials to that form.

Follows the resulting authentication exchange.

Then extracts the authorization code from the redirect to the custom URI scheme the real client expects.

After that, the flow becomes ordinary token handling.

But two design decisions around this matter more to me than simply making login work.

The password gets exactly one job

The Hubspace password is used for the initial interactive authentication.

Then the plugin forgets it.

It is never stored.

There is deliberately no:

“Save the password so we can try logging in again later.”

path in the code.

What the plugin persists is the resulting refresh token.

And that token is protected using Windows DPAPI before it is stored.

That makes the refresh token a secret belonging to this machine rather than another interesting string sitting in a configuration file.

If the refresh token eventually stops being usable, the user signs in again.

That is slightly less automatic.

I am completely comfortable with that trade.

A password I do not store is a password the plugin cannot accidentally disclose later.

There is another reason not to keep logging in

While testing my account, I discovered something else useful:

Afero sent me an email whenever the plugin performed a fresh login.

That is actually good behavior.

A new login to the account is a security-relevant event.

I want the account owner to hear about it.

But imagine the plugin had been designed to use full authentication as routine error recovery.

Token problem?

Log in again.

Temporary service issue?

Log in again.

Restart?

Perhaps log in again for good measure.

Now the plugin itself produces a steady stream of security-alert emails.

At first, the user reads them.

Then they recognize them.

Then they stop reading them.

And suddenly the software has accomplished something considerably worse than being annoying.

It has trained its own user to ignore the security messages they should pay attention to.

That is backwards.

So a full login is not routine maintenance.

The refresh token exists specifically so the plugin can maintain access without continually recreating the session.

A fresh login remains unusual enough that the email still means something.

Security alerts should be rare enough to remain interesting.

This one really is cloud-dependent

Unlike Wemo or Lutron, there is no local device-control protocol underneath this plugin that I can simply switch to when I decide I would rather not involve the Internet.

Current Hubspace integrations are fundamentally talking to Afero’s cloud.

That means this plugin needs:

The Internet.

Afero.

The Hubspace account.

And the relevant services to continue operating.

I would prefer local control.

But preferring something does not make an undocumented local protocol magically appear.

So the design goal here is different.

Where I cannot eliminate the cloud dependency, I can at least avoid making it larger than necessary.

Do not store the password.

Do not perform unnecessary logins.

Protect the credential that must be stored.

Be explicit about what comes from the cloud and what is inferred locally.

And degrade honestly when information is unavailable.

The cloud knows the MAC address

One especially interesting example of that last point is network identity.

In the account data I captured, Hubspace reported each Wi-Fi device’s MAC address.

What it did not report was its local IP address.

Not in the ordinary device record.

Not tucked into the metadata I inspected.

Not elsewhere in the account dump I captured.

From Afero’s perspective, that makes perfect sense.

The cloud does not particularly care whether one of my lights is:

192.168.1.74

today and:

192.168.1.91

next month.

The device makes an outbound connection to the platform.

The cloud already has its own device identity.

Its private address inside my house is not especially useful information to Afero.

It can still be useful information to HomeSeer.

Which led to a different question.

The house may already know the answer

Hubspace knows:

This device has this MAC address.

Windows may know:

That MAC address is currently at this IP address.

The HomeSeer machine is sitting on the same local network as those devices.

If it has communicated with them recently, or if the network stack has otherwise learned about them, the operating system’s own neighbor table may already contain the MAC-to-IP relationship.

So the plugin joins the two pieces of information.

From the cloud:

Device identity and MAC address.

From the local machine:

Current IP associated with that MAC, if known.

No additional cloud API.

No broad network scan.

No database of guesses.

Just two systems contributing the information each one actually has.

I like that arrangement.

Best effort should mean best effort

There is an important limitation.

Neighbor-table entries are temporary.

A device that has been quiet long enough may age out.

So the plugin cannot always determine an IP address.

When it cannot, it says so.

It does not display the last address forever and quietly hope DHCP has not changed anything.

It does not invent a result to make the UI look complete.

It simply has no current answer.

That distinction matters.

Unknown

and:

Wrong

are not the same condition.

Unknown means:

I do not have enough information.

Wrong means:

Here is a confident-looking answer that may send you troubleshooting the wrong device.

I will take the blank field.

The trick escaped into Wemo

The MAC-to-IP lookup was originally created here because Hubspace gave me exactly half of the network information I wanted.

At the time, it seemed like a very Hubspace-specific solution.

Then I worked on Wemo.

Wemos can move under DHCP.

The plugin remembers the device.

The previous IP address may no longer belong to it.

But the MAC remains useful.

And suddenly the same question appeared again:

Where did this MAC go?

The neighbor-table technique now does much of the heavy lifting when the Wemo plugin needs to rediscover a device that has changed address.

It has found uses elsewhere too.

I enjoy that sort of thing.

You solve what looks like one weird problem for one particular integration.

Then six months later you realize what you actually built was a general tool waiting for another job.

What the undocumented API keeps teaching

Hubspace is probably one of the better examples in this group of why I try to distinguish:

Observed

from:

assumed.

The service has already evolved.

Community integrations have had to adapt to authentication changes, device schema changes, new function classes, and cloud behavior that occasionally changes underneath them.

That is not unusual for an internal API.

The company never promised third-party developers that this interface would remain unchanged.

We are observing a conversation intended for its own client.

That makes careful notes especially valuable.

If I captured it:

Say I captured it.

If I measured it:

Say I measured it.

If another implementation reports something similar:

Useful corroboration.

But not a replacement for verification.

An undocumented API is already uncertain enough.

It does not need help from confident comments.

What I like about this one

Hubspace is necessarily different from many of the other plugins I have built.

I cannot make it genuinely local just because local would fit my philosophy better.

The platform is cloud-first.

So instead, the interesting design choices happen around the boundaries.

Use the real authentication flow rather than inventing one.

Do not retain the password.

Protect the refresh token.

Do not repeatedly log in and train the user to ignore security alerts.

Take the MAC address the cloud provides and combine it with what the local machine already knows.

And never pretend a best-effort piece of information is authoritative merely because an empty field looks untidy.

Most importantly:

Capture what the working client actually does before writing a large body of code around what I think it does.

With documented protocols, the specification is the starting point.

With Hubspace, the working app is the specification.

At least until tomorrow.

And that is exactly why I wrote it down.