Energy bills are getting weird lately and I decided I need to know what’s my homelab’s energy usage is to make some educated decisions. I went shopping for smart plugs that can measure the usage and came back with Eve Energy.

The shop listing showed a screenshot of the iOS app where you can see usage in Watts. Seemed good enough. The device came in either HomeKit or Matter, and I decided to grab the latter, as it seemed more future-proof. This was my first Matter device, and I didn’t have a Thread Border Router, so I grabbed an Apple HomePod Mini along. back then I didn’t know what a rollercoaster of IoT I’m signing up for.

The devices came in mail, and the setup was reasonably trivial. I had to call apple support because the HomePod wouldn’t want to accept my iCloud credentials, though. They sorted it out real fast. I got my new plug in the Home app and I could turn it off and on again. I could also see the usage in the Eve’s official app. Perfect! Now I only needed to dump those numbers in my prometheus and come back in a month. So I went to see the exposed attributes of the plug…

And the only thing in there was the power state. Bummer.

Now, the Eve app gets the numbers somehow, right? A quick googling said it talks to the device out-of-band over BLE but that wouldn’t make any sense, because the numbers were updating even when I was physically away from the plug. They must have been coming in through Matter, but how?

It was time to dig into the Matter specs. Here’s what I learned.

A *fabric *is a set of nodes. Nodes can talk to each other withing the fabric, potentially subject to security restrictions. A *node *is a unique addressable resource within the fabric. Generally, that’d be the consumer device (like a spart plug) or an app on the phone. Curiously, a node can be part of several fabrics, e.g. your smart light can be part of the Apple’s HomeKit fabric and Google Home’s fabric at the same time, controllable by both.

Given that nodes within the fabric use encryption, there’s no easy way to snoop in on what they are talking about. Can you talk to the node directly? Now, there’s a problem. The smart plug uses Thread—a wireless protocol that’s effectively WiFi for IoT devices. For the plug to be part of your network, it needs a border router, a device that can talk both Thread and Ethernet (or WiFi).

A light bulb went off in my head. I thought back to my experiments with 6LoWPAN and my brain started to form the understanding of a border router. It’s easy to look up that all the Thread is based on IPv6. So, does that mean my plug had an IPv6 address? How’d I look it up? I figured that smart things must utilize mDNS, and indeed there it was, under matter.tcp. I pinged the addres and I got the reply. Uh oh. A smart wall plug is sitting in my privileged network and has a full (albeit IPv6-only) reachability. Isn’t that exciting?

I wasn’t excited. Thoughts of VLANs and firewalls skimmed past, but in the end what mattered was that the plug is in my LAN, ready to talk matter. Apple hold the encryption keys for the fabric, but those were theoretically obtainable, as Matter support in iOS states:

Once added, Apple Home transparently supports Matter accessories in the Home app, Siri, Control Center, and in third-party HomeKit apps. And, third-party Matter ecosystem apps can also easily request users’ permission to access these accessories using the MatterSupport Framework, helping promote interoperability across ecosystems MatterSupport.framework’s docs, though, are a mess. I looked far and wide, and eventually I stumbled upon this sample code for the framework (Apple didn’t provide neither an example app nor some reasonable docs). The code looked promising, but the issues looked even more promising as someone had a use case very similar to mine:

Then, you want to access the HomeKit Shared Matter Controller using the matterControllerXPCConnectBlock After this, you should be able to access the accessories using the Matter Framework. HomeKit time! Of course, macOS doesn’t support HomeKit (yeah, why would it), so I had to run the code on my actual iPhone for it to be able to talk to the non-simulated home. After going back and forth with the entitlements, I finally got something that was functional:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class MyManager: NSObject, ObservableObject, HMHomeManagerDelegate {
    private var homeManager = HMHomeManager()
    
    override init() {
        super.init()
        homeManager.delegate = self
    }
    
    func homeManager(_ manager: HMHomeManager, didUpdate status: HMHomeManagerAuthorizationStatus) {
        print("auth updated: \(status == HMHomeManagerAuthorizationStatus.authorized)")
    }
    
    func homeManagerDidUpdateHomes(_ manager: HMHomeManager) {
        let home = self.homes[0]
        
        print("got a home: \(home)")
    }
}

HMHomeManager isn’t the most straightforward of the interafaces. Upon init it always returns an empty array for homes, but soon after it will actually populate it.

From a home, you can get the list of accessories via home.accessories, and I already spied a matterNodeID property on an accessory. Now I had everything I needed to create an instance of a matter device:

1
2
3
4
5
6
7
let acc = home.accessories.first { $0.name == "Eve Socket 1" }!
let mdc = MTRDeviceController.sharedController(
    withID: home.matterControllerID as NSCopying, 
    xpcConnect:home.matterControllerXPCConnectBlock)
let d = MTRBaseDevice(
    nodeID: NSNumber(value: (acc.matterNodeID)!),
    controller: mdc)

Great, what’s next? Let’s get back to the Matter spec.

The MTRBaseDevice represents a Matter node. Within a node there are several endpoints, and each endpoint provides some specific service based on its device type. E.g. a Hue light sensor could have 3 endpoints for a motion detector, a temperature sensor, and a light sensor. Finally, each endpoint can have clusters, which are effectively the RPC interfaces. Cluster can contain attributes (which are just some data), events (historical records for e.g. state transitions) and commands (the actual calls you can perform on an endpoint). There’s more complexity further on, but what’s important is that there’s a descriptior cluster (9.5 in the spec mentoned above), that must contain the device type, its client and server clusters, and a bunch of other reflection data. Also, endpoint 0 is a reserved one, and its descriptor knows about all the others.

Knowing that (and armed with a spec), one could do something akin to

1
2
3
4
5
6
print(try await d.readAttributes(
    withEndpointID: 0,
    clusterID: 0x0028,
    attributeID: 0x03,
    params: nil,
    queue: DispatchQueue.global()))

which returns attribute 0x3 of cluster 0x28 of endpoint 0, which according to the spec, is ProductName (a string) in a Basic Information Cluster.

And it works! I got back my smart plug, which effectively confirmed I could talk to my matter device over matter using HomeKit as a trampoline. So now I could go over the descriptor cluster and see what else it can do.

Now, supposedly there’s MTRBaseClusterDescriptor which can dofancy things like readAttributePartsList. But the async call to that never resolved for me so I got back to using readAttributes and the raw IDs from the spec.

What I was interested in was the server list (i.e. what services the plug provides). That’s endpoint 0, cluster 0x1d, attribute 0x1. It returned a list with 5 clusters. Armed with the Application Clusters spec, I went through the list.

Cluster 3: Identify. That’s a service that allows you to identify a specific hardware device. Basically, it can blink an LED.

Cluster 4: Groups. My understanding is that it allows the nodes’ endpoints to be a part of a group with its own encryption and multicast capabilities.

Cluster 6: OnOff. This is the actual switch. The commands for this one allow you to turn the plug off and on.

Cluster 0x1d: That’s the descriptor cluster I got all this info from.

Cluster 0x130afc01: Bingo!

Obviously, the 0x130afc01 number wasn’t part of the public spec. The issue with smart plugs is that there is no Matter cluster for plugs usage, so the vendors had to come up with their private cluster. Given how this was the only one left and how cryptic it looked, I was sure that’d be the one where I find my Watts. However, the problem was to find the attribute that stored the value. A quick brute-force showed that I can crawl 1 attribute in 1.5 seconds—Thread isn’t the fastest of the networks and a single IPv6 packet is further split into several frames. Bruteforcing was a no go, google had no idea what 0x130afc01 referred to.

But GitHub did! A constant named PRIVATE_CLUSTER_ID in a file eve-energy/init.lua! And, next to it, PRIVATE_ATTR_ID_WATT (good luck bruteforcing that 0x130a000a!). A quick confirmation:

1
2
3
4
5
6
try await d.readAttributes(
    withEndpointID: 1,
    clusterID: 0x130afc01,
    attributeID: 0x130a000a,
    params: nil,
    queue: DispatchQueue.global())

And I get the number I expect to see. Amazing. How di the author of that commit figure it all out? I had so many questions; did they actually brute-force it, did I miss some Eve’s memo that listed those?

Oh. I guess that works, too. Easy to get private IDs if you’re the one writing them in the product.

Finally, my trip into the world of Matter came to a conclusion. Matter is, surprizingly, a pretty nice to work with protocol (comparatively). You don’t need any hacks to talk to a matter device from an iPhone, other than needing to know private IDs for private features. Of course, you can still try and bruteforce those.

It seems that Apple’s MatterSupport allows you to register a device in a different fabric, and that’s what I’ll be looking for in the next. After all, I don’t need to get the Watts from my iPhone, there’s an app for that. I need to get that number from my server, and that means I need to make the plug talking to the other fabric. But it looks more and more doable.