<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Homelab on Jahvon Dockery</title>
    <link>https://jahvon.dev/tags/homelab/</link>
    <description>Recent content in Homelab on Jahvon Dockery</description>
    <generator>Hugo -- 0.148.1</generator>
    <language>en</language>
    <atom:link href="https://jahvon.dev/tags/homelab/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>HubExt</title>
      <link>https://jahvon.dev/architecture/hubext/</link>
      <pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate>
      <guid>https://jahvon.dev/architecture/hubext/</guid>
      <description>Smart Home API Gateway that provides a unified control plane for my smart home ecosystems. Providing device management, automation rules, and scene control across multiple platforms through a gateway.</description>
      <content:encoded><![CDATA[<style type="text/css">
     
    .notice {
        --title-color: #fff;
        --title-background-color: #6be;
        --content-color: #444;
        --content-background-color: #e7f2fa;
    }

    .notice.info {
        --title-background-color: #fb7;
        --content-background-color: #fec;
    }

    .notice.tip {
        --title-background-color: #5a5;
        --content-background-color: #efe;
    }

    .notice.warning {
        --title-background-color: #c33;
        --content-background-color: #fee;
    }

     
    @media (prefers-color-scheme:dark) {
        .notice {
            --title-color: #fff;
            --title-background-color: #069;
            --content-color: #ddd;
            --content-background-color: #023;
        }

        .notice.info {
            --title-background-color: #a50;
            --content-background-color: #420;
        }

        .notice.tip {
            --title-background-color: #363;
            --content-background-color: #121;
        }

        .notice.warning {
            --title-background-color: #800;
            --content-background-color: #400;
        }
    }

    body.dark .notice {
        --title-color: #fff;
        --title-background-color: #069;
        --content-color: #ddd;
        --content-background-color: #023;
    }

    body.dark .notice.info {
        --title-background-color: #a50;
        --content-background-color: #420;
    }

    body.dark .notice.tip {
        --title-background-color: #363;
        --content-background-color: #121;
    }

    body.dark .notice.warning {
        --title-background-color: #800;
        --content-background-color: #400;
    }

     
    .notice {
        padding: 18px;
        line-height: 24px;
        margin-bottom: 24px;
        border-radius: 4px;
        color: var(--content-color);
        background: var(--content-background-color);
    }

    .notice p:last-child {
        margin-bottom: 0
    }

     
    .notice-title {
        margin: -18px -18px 12px;
        padding: 4px 18px;
        border-radius: 4px 4px 0 0;
        font-weight: 700;
        color: var(--title-color);
        background: var(--title-background-color);
    }

     
    .icon-notice {
        display: inline-flex;
        align-self: center;
        margin-right: 8px;
    }

    .icon-notice img,
    .icon-notice svg {
        height: 1em;
        width: 1em;
        fill: currentColor;
    }

    .icon-notice img,
    .icon-notice.baseline svg {
        top: .125em;
        position: relative;
    }
</style><div class="notice info" >
    <p class="notice-title">
        <span class="icon-notice baseline">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="92 59.5 300 300">
  <path d="M292 303.25V272c0-3.516-2.734-6.25-6.25-6.25H267v-100c0-3.516-2.734-6.25-6.25-6.25h-62.5c-3.516 0-6.25 2.734-6.25 6.25V197c0 3.516 2.734 6.25 6.25 6.25H217v62.5h-18.75c-3.516 0-6.25 2.734-6.25 6.25v31.25c0 3.516 2.734 6.25 6.25 6.25h87.5c3.516 0 6.25-2.734 6.25-6.25Zm-25-175V97c0-3.516-2.734-6.25-6.25-6.25h-37.5c-3.516 0-6.25 2.734-6.25 6.25v31.25c0 3.516 2.734 6.25 6.25 6.25h37.5c3.516 0 6.25-2.734 6.25-6.25Zm125 81.25c0 82.813-67.188 150-150 150-82.813 0-150-67.188-150-150 0-82.813 67.188-150 150-150 82.813 0 150 67.188 150 150Z"/>
</svg>

        </span>Info</p><p>This page is a public overview of a closed source project. I hope to one day
open source it, but for now this is a way to share the architecture and design.</p>
<p><strong>Last updated</strong>: August 11, 2025</p></div>

<p>HubExt is a system that aggregates multiple smart home platforms and protocols into a single API gateway, providing unified device management, automation rules, and scene control across different ecosystems. The architecture handles real-time device synchronization, event processing, and cross-platform automation orchestration powered by <a href="/tags/flow/">flow</a> and an eventual HubExt Dashboard.</p>
<h2 id="design-philosophy">Design Philosophy</h2>
<ul>
<li><strong>Unified Control Plane</strong>: Provide a single API for managing devices across multiple smart home platforms.</li>
<li><strong>Extensible Architecture</strong>: Support new platforms and devices through modular integration layers.</li>
<li><strong>Declarative Automation</strong>: Use a rules engine to define automation logic in a platform-agnostic way.</li>
</ul>
<h2 id="system-architecture">System Architecture</h2>
<p>The gateway operates as a centralized control plane that bridges local smart home protocols with cloud services while maintaining responsive local control capabilities.</p>
<p><img alt="HubExt System Overview" loading="lazy" src="/images/hubext-system-overview.png"></p>
<h2 id="technical-stack">Technical Stack</h2>
<ul>
<li><strong>Go</strong> - Backend <a href="https://github.com/gin-gonic/gin">Gin</a> API server and core logic</li>
<li><strong>TypeScript/React</strong> - WIP Dashboard for device management and automation configuration</li>
<li><strong>Flow Powered Workflows</strong> - Declarative workflows build on top of the <a href="https://flowexec.io/">flow</a> platform for observability and management via the CLI and UI</li>
</ul>
<h2 id="current-platform-integrations">Current Platform Integrations</h2>
<p><strong>Hubitat Hub Integration</strong></p>
<ul>
<li>Protocol: HTTP REST API using Makers API</li>
<li>Authentication: Access token with App ID</li>
<li>Communication: Local network for minimal latency</li>
<li>Event Handling: Webhook-based real-time device updates</li>
</ul>
<p><strong>Flair HVAC Integration</strong></p>
<ul>
<li>Protocol: OAuth 2.0 REST API with automatic token refresh</li>
<li>Components: Structures, rooms, HVAC units, sensor bridges</li>
<li>Capabilities: Mini-split control, room temperature monitoring</li>
</ul>
<p><strong>Weather Service Integration</strong></p>
<ul>
<li>Provider: WeatherAPI.com with API key authentication</li>
<li>Data Points: Temperature, humidity, conditions, feels-like temperature</li>
<li>Caching: Local cache with 30-minute refresh intervals</li>
</ul>
<p><strong>Google Calendar Integration</strong></p>
<ul>
<li>Protocol: OAuth 2.0 REST API with automatic token refresh</li>
<li>Use Case: Event-based automation triggers and conditions</li>
</ul>
<p><strong>Pushover Notifications</strong></p>
<ul>
<li>Protocol: HTTP POST requests to Pushover API</li>
<li>Use Case: Real-time alerts for device events and automation triggers</li>
</ul>
<p><strong>AI Integration</strong> (WIP)</p>
<ul>
<li>Provider: Anthropic API for natural language processing</li>
<li>Use Case: Natural language automation rule creation and device control</li>
</ul>
<h3 id="device-management-system">Device Management System</h3>
<p>Most of my devices are registered in the Hubitat platform, which provides a local API for device management.
I have also been evaluating Home Assistant as a potential alternative for future integrations but have not yet migrated.
The core requirements for the device management system include:</p>
<ul>
<li><strong>Unified Device Model</strong>: Abstract representation of devices across platforms</li>
<li><strong>Capability-Based Architecture</strong>: Devices expose capabilities like switches, sensors, and thermostats</li>
<li><strong>Room Organization</strong>: Devices are grouped by rooms with hierarchical structure</li>
</ul>
<p>Device states are synchronized into local cache storage, providing fast API responses while maintaining eventual consistency with upstream platforms.</p>
<p><strong>Abstract Device Interface</strong></p>
<p>All devices implement a common interface to ensure consistent interaction across platforms:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">type</span> Device <span style="color:#e67e80">interface</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#b2c98f">ID</span>() <span style="color:#dbbc7f">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">Name</span>() <span style="color:#dbbc7f">string</span>
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">Room</span>() room.Room
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">MatchesName</span>(<span style="color:#dbbc7f">string</span>) <span style="color:#dbbc7f">bool</span>
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">Capabilities</span>() []CapabilityName
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">As</span>(CapabilityName) Capability
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This interface is implemented for each platform / device type once and reused across the system.
It allows for flexible device management and interaction without needing to know the underlying platform details.</p>
<h4 id="capability-based-controls">Capability-Based Controls</h4>
<p>Devices expose capabilities that define their functionality, allowing for flexible control and automation. For instance:</p>
<ul>
<li>Switch capabilities for on/off control</li>
<li>Sensor capabilities for environmental data</li>
<li>Thermostat capabilities for HVAC control</li>
<li>Button capabilities for trigger events</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">type</span> Capability <span style="color:#e67e80">interface</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">Name</span>() CapabilityName
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">IsStateful</span>() <span style="color:#dbbc7f">bool</span>
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">CurrentState</span>() CapabilityState
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">Merge</span>(Capability)
</span></span><span style="display:flex;"><span>	<span style="color:#b2c98f">SendCommand</span>(<span style="color:#dbbc7f">string</span>) <span style="color:#dbbc7f">error</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h4 id="room-organization">Room Organization</h4>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">type</span> Room <span style="color:#e67e80">struct</span> {
</span></span><span style="display:flex;"><span>	Name    <span style="color:#dbbc7f">string</span>   <span style="color:#b2c98f">`json:&#34;name,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>	Aliases []<span style="color:#dbbc7f">string</span> <span style="color:#b2c98f">`json:&#34;aliases,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>	Groups  []Group  <span style="color:#b2c98f">`json:&#34;groups,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>	Rank    <span style="color:#dbbc7f">uint</span>     <span style="color:#b2c98f">`json:&#34;rank,omitempty&#34;`</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><ul>
<li>Hierarchical room structure with groups and aliases</li>
<li>Device-to-room mapping for contextual automation</li>
<li>Room-based filtering and bulk operations functionality</li>
</ul>
<h3 id="automation-engine">Automation Engine</h3>
<h4 id="event-processing">Event Processing</h4>
<ol>
<li>Device state change triggers webhook OR data sync causes event generation</li>
<li>Event parsed and validated</li>
<li>Matching rules evaluated</li>
<li>Actions executed across relevant platforms</li>
<li>Results logged and metrics updated</li>
</ol>
<h4 id="rules-engine">Rules Engine</h4>
<p>Event-driven automation with logic defined in Go handlers.</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">type</span> Rule <span style="color:#e67e80">struct</span> {
</span></span><span style="display:flex;"><span>	Name       <span style="color:#dbbc7f">string</span>
</span></span><span style="display:flex;"><span>	Aliases    []<span style="color:#dbbc7f">string</span>
</span></span><span style="display:flex;"><span>	HandleFunc Handler
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#e67e80">type</span> Handler <span style="color:#e67e80">func</span>(Event, room.List, device.List) (handled <span style="color:#dbbc7f">bool</span>, err <span style="color:#dbbc7f">error</span>)
</span></span></code></pre></div><p>The handle function is responsible for processing events and executing actions based on the rule&rsquo;s logic. At the moment, all events
flow through all rule handlers so they must handle filtering. Some smarter filter logic would be nice to have here but is unecessary at the current scale.</p>
<div class="notice note" >
    <p class="notice-title">
        <span class="icon-notice baseline">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 128 300 300">
  <path d="M150 128c82.813 0 150 67.188 150 150 0 82.813-67.188 150-150 150C67.187 428 0 360.812 0 278c0-82.813 67.188-150 150-150Zm25 243.555v-37.11c0-3.515-2.734-6.445-6.055-6.445h-37.5c-3.515 0-6.445 2.93-6.445 6.445v37.11c0 3.515 2.93 6.445 6.445 6.445h37.5c3.32 0 6.055-2.93 6.055-6.445Zm-.39-67.188 3.515-121.289c0-1.367-.586-2.734-1.953-3.516-1.172-.976-2.93-1.562-4.688-1.562h-42.968c-1.758 0-3.516.586-4.688 1.563-1.367.78-1.953 2.148-1.953 3.515l3.32 121.29c0 2.734 2.93 4.882 6.64 4.882h36.134c3.515 0 6.445-2.148 6.64-4.883Z"/>
</svg>

        </span>Note</p><p>The rules engine design is still a work in progress. The goal is to provide a seamless integration with
external rules engines via the platform integrations but those aren&rsquo;t consistently exposed. The cufrrent implementation
is a basic event-driven system that evaluates rules based on device state changes and executes actions across platforms but
the definition can be streamlined further.</p></div>

<h4 id="scene-management">Scene Management</h4>
<p>Scenes follow a similar structure to rules but are predefined automation scenarios that coordinate multiple devices across platforms.</p>
<p>When defined in Go, the handler function is responsible for executing the scene by sending commands to the relevant devices.</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-go" data-lang="go"><span style="display:flex;"><span><span style="color:#e67e80">func</span> <span style="color:#b2c98f">pbChillHandler</span>(_ room.List, curDevices device.List) (<span style="color:#dbbc7f">bool</span>, <span style="color:#dbbc7f">error</span>) {
</span></span><span style="display:flex;"><span>	tableLamp <span style="color:#7a8478">:=</span> curDevices.<span style="color:#b2c98f">GetDevice</span>(registry.PrimaryBedroomTableLamp)
</span></span><span style="display:flex;"><span>	ceilingLight <span style="color:#7a8478">:=</span> curDevices.<span style="color:#b2c98f">GetDevice</span>(registry.PrimaryBedroomCeilingLight)
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>	<span style="color:#e67e80">switch</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#e67e80">case</span> timeframe.<span style="color:#b2c98f">CurrentDay</span>().<span style="color:#b2c98f">IsWeekday</span>() <span style="color:#7a8478">&amp;&amp;</span> timeframe.<span style="color:#b2c98f">Night</span>().<span style="color:#b2c98f">CurTimeInFrame</span>():
</span></span><span style="display:flex;"><span>		<span style="color:#e67e80">if</span> err <span style="color:#7a8478">:=</span> tableLamp.<span style="color:#b2c98f">As</span>(device.SwitchName).<span style="color:#b2c98f">SendCommand</span>(device.OnCommand); err <span style="color:#7a8478">!=</span> <span style="color:#e67e80">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#e67e80">return</span> <span style="color:#e67e80">false</span>, err
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#e67e80">if</span> err <span style="color:#7a8478">:=</span> ceilingLight.<span style="color:#b2c98f">As</span>(device.SwitchName).<span style="color:#b2c98f">SendCommand</span>(device.OffCommand); err <span style="color:#7a8478">!=</span> <span style="color:#e67e80">nil</span> {
</span></span><span style="display:flex;"><span>			<span style="color:#e67e80">return</span> <span style="color:#e67e80">false</span>, err
</span></span><span style="display:flex;"><span>		}
</span></span><span style="display:flex;"><span>		<span style="color:#e67e80">return</span> <span style="color:#e67e80">true</span>, <span style="color:#e67e80">nil</span>
</span></span><span style="display:flex;"><span>	<span style="color:#e67e80">default</span>:
</span></span><span style="display:flex;"><span>		<span style="color:#e67e80">return</span> <span style="color:#e67e80">false</span>, <span style="color:#e67e80">nil</span>
</span></span><span style="display:flex;"><span>	}
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>This currently feels a bit clunky, the end goal is to get to a point where these can be defined in simple YAML like:</p>
<div class="highlight"><pre tabindex="0" style="color:#d6cbb4;background-color:#252b2e;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7a8478">scenes</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#7a8478">PBRChill</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">devices</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#7a8478">room</span>: <span style="color:#b2c98f">&#34;primary_bedroom&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#7a8478">type</span>: <span style="color:#b2c98f">&#34;switch&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#7a8478">action</span>: <span style="color:#b2c98f">&#34;dim_to_30&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#7a8478">room</span>: <span style="color:#b2c98f">&#34;primary_bedroom&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#7a8478">type</span>: <span style="color:#b2c98f">&#34;hvac&#34;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#7a8478">action</span>: <span style="color:#b2c98f">&#34;cool_to_68&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#7a8478">window</span>: <span style="color:#b2c98f">&#34;weekday night&#34;</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
  </channel>
</rss>
