<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ HAP - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ HAP - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 05 May 2026 22:20:03 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/hap/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Smart HomeKit Virtual Light in Go ]]>
                </title>
                <description>
                    <![CDATA[ Recently, I wanted to understand how smart home devices actually work. When you scan a QR code and a light appears in your Home app, what's really happening? When you tap "on", what bytes travel across your network? The best way I know to understand... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-smart-homekit-virtual-light-in-go/</link>
                <guid isPermaLink="false">69449f453cbff85be8965f37</guid>
                
                    <category>
                        <![CDATA[ HAP ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Go Language ]]>
                    </category>
                
                    <category>
                        <![CDATA[ homekit ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Apple ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Matter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ protocols ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Rez Moss ]]>
                </dc:creator>
                <pubDate>Fri, 19 Dec 2025 00:41:41 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1766104866742/8d8c0158-95d0-493b-a311-cd99189654e1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Recently, I wanted to understand how smart home devices actually work. When you scan a QR code and a light appears in your Home app, what's really happening? When you tap "on", what bytes travel across your network?</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1765414117210/e65d5768-792d-4aae-8669-db0ac7a9c60d.png" alt="Virtual HomeKit Light QR code" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The best way I know to understand something is to build it, so I created a virtual HomeKit light in Go. And in this tutorial, I’ll walk you through how I went about it. We’ll pull back the curtain on smart home protocols so you understand how they work, in depth. Let’s dive in.</p>
<h3 id="heading-what-well-cover">What we’ll cover:</h3>
<ol>
<li><p><a class="post-section-overview" href="#heading-what-homekit-actually-is">What HomeKit Actually Is</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-smart-home-protocol-landscape">The Smart Home Protocol Landscape</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-homekit-discovery-works">How HomeKit Discovery Works</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-pairing-process-what-happens-when-you-scan-the-qr-code">The Pairing Process: What Happens When You Scan the QR Code</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-setup-uri-whats-in-that-qr-code">The Setup URI: What's in That QR Code?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-happens-when-you-toggle-the-light">What Happens When You Toggle the Light</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-accessory-database-model">The Accessory Database Model</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-persisting-pairing-data">Persisting Pairing Data</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-event-notifications">Event Notifications</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-complete-implementation">The Complete Implementation</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-i-learned">What I Learned</a></p>
</li>
</ol>
<h2 id="heading-what-youll-need">What You'll Need</h2>
<p>Before we start building, let's make sure you have the right setup. This project requires two things:</p>
<ol>
<li><p><strong>Go 1.21 or later</strong>: We're using some modern Go features, and the brutella/HAP library works best with recent versions. You can check your version with <code>go version</code>. If you need to upgrade, grab the latest from go.dev</p>
</li>
<li><p><strong>An Apple HomeKit environment</strong>: This means an iPhone or iPad running iOS 15+ with the Home app. You'll also want to be on the same WiFi network as the machine running your virtual light. HomeKit is entirely local, so your phone needs to be able to reach your development machine directly.</p>
</li>
</ol>
<p>One thing that tripped me up initially is that if you’re running this on a Linux server or inside a container, make sure mDNS traffic isn’t being blocked. Your firewall needs to allow UDP port 5353 (for mDNS discovery) and whatever port your accessory runs on (we'll use 51826). On a Mac this usually just works.</p>
<h2 id="heading-what-homekit-actually-is">What HomeKit Actually Is</h2>
<p>HomeKit is Apple's smart home framework. It's comprised of three things:</p>
<ol>
<li><p><strong>a protocol (HAP)</strong> that defines how devices talk to each other,</p>
</li>
<li><p><strong>a security model</strong> that encrypts and authenticates everything,</p>
</li>
<li><p><strong>and an ecosystem</strong> (the Home app, Siri, automations)</p>
</li>
</ol>
<p>Here, we’ll be focused on the protocol layer. We're building something that speaks HAP well enough that Apple's ecosystem accepts it as a real accessory.</p>
<h2 id="heading-the-smart-home-protocol-landscape">The Smart Home Protocol Landscape</h2>
<p>Before getting started, let's understand what we're dealing with. There are two protocols at play here:</p>
<ol>
<li><p><strong>HomeKit Accessory Protocol (HAP):</strong> Apple's original smart home protocol from 2014. It runs over your local WiFi network, uses mDNS for discovery, and encrypts everything with Curve25519 and ChaCha20-Poly1305. Every HomeKit device you've ever used speaks HAP.</p>
</li>
<li><p><strong>Matter</strong>: The new industry standard (2022) backed by Apple, Google, Amazon, and others. Matter is actually built on many of the same cryptographic primitives as HAP. When Apple added Matter support, they essentially made HomeKit bilingual, as it can speak both protocols.</p>
</li>
</ol>
<p>Here's what's interesting: Matter devices that connect to Apple Home still end up being controlled through HomeKit's infrastructure. Matter is the pairing and discovery layer, but once a device is in your Home, Apple's ecosystem takes over.</p>
<p>For this project, I'm using the HAP protocol directly via the <code>brutella/hap</code> library. This lets us see exactly what's happening without Matter's additional abstraction layer.</p>
<h2 id="heading-how-homekit-discovery-works">How HomeKit Discovery Works</h2>
<p>When you run a HomeKit accessory on your network, it doesn't just sit there waiting. It actively announces itself using <strong>mDNS</strong> (multicast DNS), also called Bonjour on Apple platforms.</p>
<p>The accessory broadcasts a service record that looks like this:</p>
<pre><code class="lang-plaintext">_hap._tcp.local.
  name: Virtual Light._hap._tcp.local.
  port: 51826
  txt: 
    c#=1          // config number (changes trigger rediscovery)
    ff=0          // feature flags
    id=XX:XX:XX   // device ID (like a MAC address)
    md=Virtual Light  // model name
    pv=1.1        // protocol version
    s#=1          // state number
    sf=1          // status flag (1=not paired, 0=paired)
    ci=5          // category (5=lightbulb)
    sh=XXXXXX     // setup hash
</code></pre>
<p>Your iPhone is constantly listening for <code>_hap._tcp.local.</code> broadcasts. When it sees one with <code>sf=1</code> (unpaired), it shows up in "Add Accessory" as available.</p>
<p>Let's see this in code. Here's the minimal server setup:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>

    <span class="hljs-string">"github.com/brutella/hap"</span>
    <span class="hljs-string">"github.com/brutella/hap/accessory"</span>
)

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    light := accessory.NewLightbulb(accessory.Info{
        Name:         <span class="hljs-string">"Virtual Light"</span>,
        Manufacturer: <span class="hljs-string">"My Smart Home"</span>,
    })

    server, err := hap.NewServer(hap.NewFsStore(<span class="hljs-string">"./data"</span>), light.A)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatal(err)
    }

    server.Pin = <span class="hljs-string">"00102003"</span>
    server.Addr = <span class="hljs-string">":51826"</span>

    server.ListenAndServe(context.Background())
}
</code></pre>
<p>When <code>ListenAndServe</code> runs, it:</p>
<ol>
<li><p>Generates a unique device ID if one doesn't exist</p>
</li>
<li><p>Starts listening on port 51826</p>
</li>
<li><p>Registers the mDNS service record</p>
</li>
<li><p>Waits for connections</p>
</li>
</ol>
<p>At this point, your iPhone can discover it. But what happens when you try to pair it?</p>
<h2 id="heading-the-pairing-process-what-happens-when-you-scan-the-qr-code">The Pairing Process: What Happens When You Scan the QR Code</h2>
<p>This is where it gets interesting. HomeKit uses the <strong>SRP (Secure Remote Password)</strong> protocol for pairing. It’s the same protocol used in things like 1Passwords authentication.</p>
<p>When you scan the QR code or enter the PIN, here's the actual sequence:</p>
<h3 id="heading-step-1-pair-setup-m1-ios-accessory">Step 1: Pair Setup M1 (iOS → Accessory)</h3>
<pre><code class="lang-plaintext">iOS sends: { method: "pair-setup", state: 1 }
</code></pre>
<p>Your phone initiates pairing, telling the accessory "I want to pair with you."</p>
<h3 id="heading-step-2-pair-setup-m2-accessory-ios">Step 2: Pair Setup M2 (Accessory → iOS)</h3>
<pre><code class="lang-plaintext">Accessory sends: { 
  state: 2,
  salt: &lt;16 random bytes&gt;,
  public_key: &lt;SRP public key B&gt;
}
</code></pre>
<p>The accessory generates an SRP salt and public key. The PIN code you entered isn't sent over the network – instead, it's used to derive a verifier locally.</p>
<h3 id="heading-step-3-pair-setup-m3-ios-accessory">Step 3: Pair Setup M3 (iOS → Accessory)</h3>
<pre><code class="lang-plaintext">iOS sends: {
  state: 3,
  public_key: &lt;SRP public key A&gt;,
  proof: &lt;SRP proof M1&gt;
}
</code></pre>
<p>Your iPhone uses the PIN to compute its own SRP values and sends a proof that it knows the PIN.</p>
<h3 id="heading-step-4-pair-setup-m4-accessory-ios">Step 4: Pair Setup M4 (Accessory → iOS)</h3>
<pre><code class="lang-plaintext">Accessory sends: {
  state: 4,
  proof: &lt;SRP proof M2&gt;
}
</code></pre>
<p>The accessory verifies the proof. If the PIN was wrong, pairing fails here. If correct, it sends its own proof back.</p>
<h3 id="heading-step-5-6-key-exchange">Step 5-6: Key Exchange</h3>
<p>Now both sides have a shared secret derived from SRP. They use this to establish an encrypted channel and exchange long term Ed25519 public keys. These keys are stored permanently. This is why your lights still work after rebooting your router.</p>
<p>The whole dance takes about 2 seconds. After this, <code>sf</code> in the mDNS record changes from <code>1</code> to <code>0</code> and the accessory disappears from "Add Accessory".</p>
<h2 id="heading-the-setup-uri-whats-in-that-qr-code">The Setup URI: What's in That QR Code?</h2>
<p>The QR code contains a URI that encodes everything needed for pairing:</p>
<pre><code class="lang-plaintext">X-HM://0ABCDEFGH1234
        ^^^^^^^^^^^^
        |       |
        |       +-- Setup ID (4 chars)
        +---------- Encoded payload (9 chars, base-36)
</code></pre>
<p>The payload packs three things into 45 bits:</p>
<ol>
<li><p><strong>Category:</strong> what type of accessory this is (5 = lightbulb, 6 = outlet, 10 = thermostat, and so on)</p>
</li>
<li><p><strong>Flags:</strong> how the accessory can pair (2 = supports IP ,wifi pairing , 4 = supports BLE pairing , 6 = supports both)</p>
</li>
<li><p><strong>PIN code</strong> as integer</p>
</li>
</ol>
<p>This lets your iPhone know what icon to show and the PIN to use, all from scanning a single QR code.</p>
<pre><code class="lang-go"><span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">generateSetupURI</span><span class="hljs-params">(pin, setupID <span class="hljs-keyword">string</span>, category <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-comment">// PIN "00102003" becomes integer 102003</span>
    <span class="hljs-keyword">var</span> pinInt <span class="hljs-keyword">uint64</span>
    <span class="hljs-keyword">for</span> _, c := <span class="hljs-keyword">range</span> pin {
        <span class="hljs-keyword">if</span> c &gt;= <span class="hljs-string">'0'</span> &amp;&amp; c &lt;= <span class="hljs-string">'9'</span> {
            pinInt = pinInt*<span class="hljs-number">10</span> + <span class="hljs-keyword">uint64</span>(c-<span class="hljs-string">'0'</span>)
        }
    }

    <span class="hljs-comment">// Bit layout:</span>
    <span class="hljs-comment">// [39:32] = category (5 = lightbulb)</span>
    <span class="hljs-comment">// [31:28] = flags (2 = IP pairing supported)</span>
    <span class="hljs-comment">// [26:0]  = PIN code</span>
    payload := (<span class="hljs-keyword">uint64</span>(category) &lt;&lt; <span class="hljs-number">32</span>) | (<span class="hljs-number">2</span> &lt;&lt; <span class="hljs-number">28</span>) | (pinInt &amp; <span class="hljs-number">0x7FFFFFF</span>)

    <span class="hljs-comment">// Encode as base-36 (0-9, A-Z)</span>
    <span class="hljs-keyword">const</span> chars = <span class="hljs-string">"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"</span>
    encoded := <span class="hljs-string">""</span>
    <span class="hljs-keyword">for</span> payload &gt; <span class="hljs-number">0</span> {
        encoded = <span class="hljs-keyword">string</span>(chars[payload%<span class="hljs-number">36</span>]) + encoded
        payload /= <span class="hljs-number">36</span>
    }

    <span class="hljs-keyword">for</span> <span class="hljs-built_in">len</span>(encoded) &lt; <span class="hljs-number">9</span> {
        encoded = <span class="hljs-string">"0"</span> + encoded
    }

    <span class="hljs-keyword">return</span> <span class="hljs-string">"X-HM://"</span> + encoded + setupID
}
</code></pre>
<p>When your iPhone camera sees <code>X-HM://</code>, it knows this is a HomeKit code. It decodes the payload to extract the category (so it can show the right icon) and the PIN (so you don't have to type it). The setup ID helps with identification when multiple unpaired accessories are on the network.</p>
<h2 id="heading-what-happens-when-you-toggle-the-light">What Happens When You Toggle the Light</h2>
<p>Now for the part I was most curious about. When you tap the light button in the Home app, what actually travels across your network?</p>
<h3 id="heading-step-1-encrypted-session">Step 1: Encrypted Session</h3>
<p>Your iPhone doesn't just send commands in plaintext. Every paired session uses the longterm keys exchanged during pairing to establish a session key. All communication is encrypted with ChaCha20Poly1305.</p>
<h3 id="heading-step-2-hap-request">Step 2: HAP Request</h3>
<p>Inside the encrypted channel, HomeKit uses a simple HTTP like protocol. A "turn on" command looks like this:</p>
<pre><code class="lang-plaintext">PUT /characteristics HTTP/1.1
Host: Virtual Light._hap._tcp.local
Content-Type: application/hap+json

{
  "characteristics": [{
    "aid": 1,        // accessory ID
    "iid": 10,       // instance ID (the "On" characteristic)
    "value": true    // new state
  }]
}
</code></pre>
<h3 id="heading-step-3-accessory-response">Step 3: Accessory Response</h3>
<p>The accessory processes the request and responds like this:</p>
<pre><code class="lang-plaintext">HTTP/1.1 204 No Content
</code></pre>
<p>If something went wrong, it'll return a status object with an error code.</p>
<p>In our Go code, we hook into this with a callback:</p>
<pre><code class="lang-go">light.Lightbulb.On.OnValueRemoteUpdate(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(on <span class="hljs-keyword">bool</span>)</span></span> {
    <span class="hljs-keyword">if</span> on {
        fmt.Println(<span class="hljs-string">"💡 Light ON"</span>)
    } <span class="hljs-keyword">else</span> {
        fmt.Println(<span class="hljs-string">"💡 Light OFF"</span>)
    }
})
</code></pre>
<p>This callback fires when the <code>value</code> in that PUT request changes. The <code>brutella/hap</code> library handles all the decryption, parsing, and response generation.</p>
<h2 id="heading-the-accessory-database-model">The Accessory Database Model</h2>
<p>HomeKit organizes everything into a hierarchy:</p>
<pre><code class="lang-plaintext">Accessory (aid=1)
└── Services
    ├── AccessoryInformation (iid=1)
    │   ├── Name (iid=2)
    │   ├── Manufacturer (iid=3)
    │   ├── Model (iid=4)
    │   └── SerialNumber (iid=5)
    │
    └── Lightbulb (iid=9)
        ├── On (iid=10)           ← boolean
        ├── Brightness (iid=11)   ← int 0-100
        └── Hue (iid=12)          ← float 0-360
</code></pre>
<p>Each characteristic has an <code>iid</code> (instance ID). When you change brightness to 75%, the PUT request targets <code>aid=1, iid=11, value=75</code>.</p>
<p>This model is why HomeKit accessories are interoperable. Every lightbulb, regardless of manufacturer, has the same characteristic structure.</p>
<h2 id="heading-persisting-pairing-data">Persisting Pairing Data</h2>
<p>When your accessory pairs with a controller (iPhone), it stores:</p>
<ul>
<li><p>The controller's Ed25519 public key</p>
</li>
<li><p>A controller ID (36chars UUID)</p>
</li>
<li><p>Permission level (admin or regular user)</p>
</li>
</ul>
<p>The accessory also has its own keypairs that must persist across restarts. If you lose this, all paired controllers become orphaned – that is, they think they’re paired, but the accessory doesn't recognize them.</p>
<p>As mentioned earlier, we need to save pairing info so that if the app/device restarts, it can communicate with Homekit again. You could use a database, but for a single accessory, a JSON file works fine. If the process crashes mid-session, you won’t lose pairing data.</p>
<p>I wrote a simple JSON store to keep everything in one file:</p>
<pre><code class="lang-go"><span class="hljs-keyword">type</span> JSONStore <span class="hljs-keyword">struct</span> {
    path <span class="hljs-keyword">string</span>
    data <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>][]<span class="hljs-keyword">byte</span>
    mu   sync.RWMutex
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *JSONStore)</span> <span class="hljs-title">Set</span><span class="hljs-params">(key <span class="hljs-keyword">string</span>, value []<span class="hljs-keyword">byte</span>)</span> <span class="hljs-title">error</span></span> {
    s.mu.Lock()
    <span class="hljs-keyword">defer</span> s.mu.Unlock()
    s.data[key] = value
    <span class="hljs-keyword">return</span> s.save()
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *JSONStore)</span> <span class="hljs-title">Get</span><span class="hljs-params">(key <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">([]<span class="hljs-keyword">byte</span>, error)</span></span> {
    s.mu.RLock()
    <span class="hljs-keyword">defer</span> s.mu.RUnlock()
    <span class="hljs-keyword">if</span> v, ok := s.data[key]; ok {
        <span class="hljs-keyword">return</span> v, <span class="hljs-literal">nil</span>
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"key not found: %s"</span>, key)
}
</code></pre>
<p>The HAP library stores several keys:</p>
<ul>
<li><p><code>uuid</code> – accessory's unique identifier</p>
</li>
<li><p><code>public</code> / <code>private</code> – Ed25519 keypair</p>
</li>
<li><p><code>*-pairings</code> – paired controller keys</p>
</li>
</ul>
<p>If you delete this JSON file, the accessory (our virtual-light) forgets all its paired controllers. Your iPhone still thinks it's paired, but the accessory doesn't recognize it anymore – you'll see "No Response" in the Home app. The fix removes the accessory from the Home app and pairs it fresh using the QR code again.</p>
<h2 id="heading-event-notifications">Event Notifications</h2>
<p>One thing I didn't expect is that HomeKit supports push notifications from accessories. When our light state changes (maybe from a physical switch), we can notify all connected controllers:</p>
<pre><code class="lang-go">light.Lightbulb.On.SetValue(<span class="hljs-literal">true</span>)  <span class="hljs-comment">// This triggers notifications</span>
</code></pre>
<p>Under the hood, the accessory maintains persistent connections with controllers. When a characteristic changes, it sends an EVENT message:</p>
<pre><code class="lang-plaintext">EVENT/1.0 200 OK
Content-Type: application/hap+json

{
  "characteristics": [{
    "aid": 1,
    "iid": 10,
    "value": true
  }]
}
</code></pre>
<p>This is how your Home app updates in realtime when someone else turns on a light.</p>
<h2 id="heading-the-complete-implementation">The Complete Implementation</h2>
<p>Here's everything together:</p>
<pre><code class="lang-go"><span class="hljs-keyword">package</span> main

<span class="hljs-keyword">import</span> (
    <span class="hljs-string">"context"</span>
    <span class="hljs-string">"encoding/json"</span>
    <span class="hljs-string">"fmt"</span>
    <span class="hljs-string">"log"</span>
    <span class="hljs-string">"os"</span>
    <span class="hljs-string">"os/signal"</span>
    <span class="hljs-string">"sync"</span>
    <span class="hljs-string">"syscall"</span>

    <span class="hljs-string">"github.com/brutella/hap"</span>
    <span class="hljs-string">"github.com/brutella/hap/accessory"</span>
    <span class="hljs-string">"github.com/skip2/go-qrcode"</span>
)

<span class="hljs-keyword">const</span> (
    pinCode  = <span class="hljs-string">"00102003"</span>
    setupID  = <span class="hljs-string">"VLTX"</span>
    category = <span class="hljs-number">5</span>
    dbFile   = <span class="hljs-string">"data.json"</span>
)

<span class="hljs-keyword">type</span> JSONStore <span class="hljs-keyword">struct</span> {
    path <span class="hljs-keyword">string</span>
    data <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>][]<span class="hljs-keyword">byte</span>
    mu   sync.RWMutex
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">NewJSONStore</span><span class="hljs-params">(path <span class="hljs-keyword">string</span>)</span> *<span class="hljs-title">JSONStore</span></span> {
    s := &amp;JSONStore{
        path: path,
        data: <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>][]<span class="hljs-keyword">byte</span>),
    }
    s.load()
    <span class="hljs-keyword">return</span> s
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *JSONStore)</span> <span class="hljs-title">load</span><span class="hljs-params">()</span></span> {
    file, err := os.ReadFile(s.path)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span>
    }
    json.Unmarshal(file, &amp;s.data)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *JSONStore)</span> <span class="hljs-title">save</span><span class="hljs-params">()</span> <span class="hljs-title">error</span></span> {
    file, err := json.MarshalIndent(s.data, <span class="hljs-string">""</span>, <span class="hljs-string">"  "</span>)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">return</span> err
    }
    <span class="hljs-keyword">return</span> os.WriteFile(s.path, file, <span class="hljs-number">0644</span>)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *JSONStore)</span> <span class="hljs-title">Set</span><span class="hljs-params">(key <span class="hljs-keyword">string</span>, value []<span class="hljs-keyword">byte</span>)</span> <span class="hljs-title">error</span></span> {
    s.mu.Lock()
    <span class="hljs-keyword">defer</span> s.mu.Unlock()
    s.data[key] = value
    <span class="hljs-keyword">return</span> s.save()
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *JSONStore)</span> <span class="hljs-title">Get</span><span class="hljs-params">(key <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">([]<span class="hljs-keyword">byte</span>, error)</span></span> {
    s.mu.RLock()
    <span class="hljs-keyword">defer</span> s.mu.RUnlock()
    <span class="hljs-keyword">if</span> v, ok := s.data[key]; ok {
        <span class="hljs-keyword">return</span> v, <span class="hljs-literal">nil</span>
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, fmt.Errorf(<span class="hljs-string">"key not found: %s"</span>, key)
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *JSONStore)</span> <span class="hljs-title">Delete</span><span class="hljs-params">(key <span class="hljs-keyword">string</span>)</span> <span class="hljs-title">error</span></span> {
    s.mu.Lock()
    <span class="hljs-keyword">defer</span> s.mu.Unlock()
    <span class="hljs-built_in">delete</span>(s.data, key)
    <span class="hljs-keyword">return</span> s.save()
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-params">(s *JSONStore)</span> <span class="hljs-title">KeysWithSuffix</span><span class="hljs-params">(suffix <span class="hljs-keyword">string</span>)</span> <span class="hljs-params">([]<span class="hljs-keyword">string</span>, error)</span></span> {
    s.mu.RLock()
    <span class="hljs-keyword">defer</span> s.mu.RUnlock()
    <span class="hljs-keyword">var</span> keys []<span class="hljs-keyword">string</span>
    <span class="hljs-keyword">for</span> k := <span class="hljs-keyword">range</span> s.data {
        <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(k) &gt;= <span class="hljs-built_in">len</span>(suffix) &amp;&amp; k[<span class="hljs-built_in">len</span>(k)-<span class="hljs-built_in">len</span>(suffix):] == suffix {
            keys = <span class="hljs-built_in">append</span>(keys, k)
        }
    }
    <span class="hljs-keyword">return</span> keys, <span class="hljs-literal">nil</span>
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">generateSetupURI</span><span class="hljs-params">(pin, setupID <span class="hljs-keyword">string</span>, category <span class="hljs-keyword">int</span>)</span> <span class="hljs-title">string</span></span> {
    <span class="hljs-keyword">var</span> pinInt <span class="hljs-keyword">uint64</span>
    <span class="hljs-keyword">for</span> _, c := <span class="hljs-keyword">range</span> pin {
        <span class="hljs-keyword">if</span> c &gt;= <span class="hljs-string">'0'</span> &amp;&amp; c &lt;= <span class="hljs-string">'9'</span> {
            pinInt = pinInt*<span class="hljs-number">10</span> + <span class="hljs-keyword">uint64</span>(c-<span class="hljs-string">'0'</span>)
        }
    }

    payload := (<span class="hljs-keyword">uint64</span>(category) &lt;&lt; <span class="hljs-number">32</span>) | (<span class="hljs-number">2</span> &lt;&lt; <span class="hljs-number">28</span>) | (pinInt &amp; <span class="hljs-number">0x7FFFFFF</span>)

    <span class="hljs-keyword">const</span> chars = <span class="hljs-string">"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"</span>
    encoded := <span class="hljs-string">""</span>
    <span class="hljs-keyword">for</span> payload &gt; <span class="hljs-number">0</span> {
        encoded = <span class="hljs-keyword">string</span>(chars[payload%<span class="hljs-number">36</span>]) + encoded
        payload /= <span class="hljs-number">36</span>
    }

    <span class="hljs-keyword">for</span> <span class="hljs-built_in">len</span>(encoded) &lt; <span class="hljs-number">9</span> {
        encoded = <span class="hljs-string">"0"</span> + encoded
    }

    <span class="hljs-keyword">return</span> <span class="hljs-string">"X-HM://"</span> + encoded + setupID
}

<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">main</span><span class="hljs-params">()</span></span> {
    light := accessory.NewLightbulb(accessory.Info{
        Name:         <span class="hljs-string">"Virtual Light"</span>,
        Manufacturer: <span class="hljs-string">"My Smart Home"</span>,
    })

    light.Lightbulb.On.OnValueRemoteUpdate(<span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">(on <span class="hljs-keyword">bool</span>)</span></span> {
        <span class="hljs-keyword">if</span> on {
            fmt.Println(<span class="hljs-string">"💡 Light ON"</span>)
        } <span class="hljs-keyword">else</span> {
            fmt.Println(<span class="hljs-string">"💡 Light OFF"</span>)
        }
    })

    store := NewJSONStore(dbFile)

    server, err := hap.NewServer(store, light.A)
    <span class="hljs-keyword">if</span> err != <span class="hljs-literal">nil</span> {
        log.Fatal(err)
    }

    server.Pin = pinCode
    server.SetupId = setupID
    server.Addr = <span class="hljs-string">":51826"</span>

    fmt.Println(<span class="hljs-string">"=============================================="</span>)
    fmt.Println(<span class="hljs-string">"       Virtual HomeKit Light"</span>)
    fmt.Println(<span class="hljs-string">"=============================================="</span>)
    fmt.Println(<span class="hljs-string">"PIN: 001-02-003"</span>)
    fmt.Println()

    setupURI := generateSetupURI(pinCode, setupID, category)
    <span class="hljs-keyword">if</span> qr, err := qrcode.New(setupURI, qrcode.Medium); err == <span class="hljs-literal">nil</span> {
        fmt.Println(qr.ToSmallString(<span class="hljs-literal">false</span>))
    }

    fmt.Println(<span class="hljs-string">"Manual: Home app → + → More Options → Virtual Light"</span>)
    fmt.Printf(<span class="hljs-string">"Data stored in: %s\n"</span>, dbFile)
    fmt.Println(<span class="hljs-string">"=============================================="</span>)

    ctx, cancel := context.WithCancel(context.Background())
    <span class="hljs-keyword">go</span> <span class="hljs-function"><span class="hljs-keyword">func</span><span class="hljs-params">()</span></span> {
        c := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">chan</span> os.Signal, <span class="hljs-number">1</span>)
        signal.Notify(c, os.Interrupt, syscall.SIGTERM)
        &lt;-c
        cancel()
    }()

    fmt.Println(<span class="hljs-string">"Running... (Ctrl+C to stop)"</span>)
    server.ListenAndServe(ctx)
}
</code></pre>
<p>Run it, pair it, and watch the terminal as you toggle from your phone. Each "💡 Light ON" is the end of an encrypted request that traveled from your phone, through your router, to this Go process.</p>
<h2 id="heading-what-i-learned">What I Learned</h2>
<p>Building this cleared up several things I'd been fuzzy on:</p>
<ol>
<li><p><strong>HomeKit is entirely local.</strong> There are no cloud servers involved in controlling devices – your commands go directly from phone to device over your LAN. This is why HomeKit devices work when your internet is down.</p>
</li>
<li><p><strong>The security model is solid.</strong> SRP for pairing means the PIN never crosses the network. Ed25519 + ChaCha20 for sessions means that even someone sniffing your WiFi sees only encrypted blobs.</p>
</li>
<li><p><strong>Matter doesn't replace HAP.</strong> At least not in Apple's ecosystem. Matter handles discovery and pairing across ecosystems, but Apple Home still uses HAP concepts internally.</p>
</li>
<li><p><strong>The protocol is HTTPish.</strong> Once you get past the encryption, it’s just PUT/GET requests with JSON bodies – surprisingly approachable.</p>
</li>
</ol>
<h3 id="heading-thanks-for-reading">Thanks for reading!</h3>
<p>The <a target="_blank" href="https://github.com/rezmoss/virtual-light">code is here</a> if you want to experiment yourself. You could try adding brightness control, or create a switch instead of a light. The best way to understand a protocol is to speak it ;)</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
