--- title: "Home Service" date: 2026-05-06 draft: false description: "Discord slash-command bot for home automation, built with Go microservices, gRPC, and a local LLM for natural language intent detection." tags: ["go", "grpc", "protobuf", "microservices", "homeautomation", "ollama", "opentelemetry"] github: "https://gitea.nik4nao.com/nik/home-services" url: "" --- ## Overview A Go microservice system that controls Home Assistant devices through Discord slash commands. Direct commands (`/light`, `/switch`) go straight to a typed gRPC gateway. Free-form natural language (`/ai query`) routes through a local LLM (Ollama) for intent extraction before dispatching the resolved action. All three services run on my homelab Kubernetes cluster, deployed via ArgoCD. ## Architecture ``` Discord user │ ├── /light, /switch ──(gRPC / mTLS)──▶ ha-gateway ──(REST)──▶ Home Assistant │ └── /ai query ────────(gRPC / mTLS)──▶ ai-gateway ──(HTTP)──▶ Ollama │ (structured JSON intent) └──(gRPC / mTLS)──▶ ha-gateway ──(REST)──▶ Home Assistant ``` Three services, each its own Go module, communicating over mTLS-secured gRPC. Proto definitions live in a shared `gen` module compiled with `buf`. ## Stack - **Language:** Go — multi-module workspace (`go.work`) - **API layer:** gRPC + Protocol Buffers, schema managed with buf - **Auth:** mTLS between all services — certificates provisioned by cert-manager - **LLM:** Ollama (local, self-hosted) — structured JSON intent extraction - **External APIs:** Home Assistant REST API, Discord Gateway API - **Observability:** OpenTelemetry instrumentation in every service, traces exported to Tempo - **CI/CD:** Gitea Actions — multi-arch image builds (amd64 + arm64) on every push to `main` - **Deployment:** K3s via ArgoCD, credentials managed with Sealed Secrets ## Architecture Pattern Each service follows a hexagonal (ports and adapters) layout: - `core/domain` — domain types (intents, lights, entities), no framework dependencies - `core/ports` — interfaces the app layer depends on (driven ports) - `adapters/primary` — inbound adapters: gRPC server, Discord slash-command handler - `adapters/secondary` — outbound adapters: Home Assistant REST client, Ollama HTTP client, gRPC clients Dependencies point inward — adapters depend on ports, ports know nothing about adapters. ## Highlights - `ai-gateway` prompts Ollama with user text and a cached list of known HA lights, parses the model's structured JSON response as an intent, and executes it through `ha-gateway` — unsupported or malformed responses return a plain explanation without taking action - Light discovery results are cached in `ai-gateway` with a configurable TTL, reducing round-trips to `ha-gateway` on every AI query - mTLS enforced on all gRPC channels — `TLS_DIR` points each service to its cert bundle; certificates are issued by the cluster's internal CA via cert-manager - `discord-bot` tracks in-flight AI requests so graceful shutdown waits briefly before terminating, avoiding dropped responses - OpenTelemetry spans propagated across all three service boundaries — full distributed traces visible in Grafana Tempo - Multi-arch images (amd64 + arm64) built on every push via Gitea Actions and pushed to the self-hosted container registry - Test coverage at the domain and adapter layer in each service ## Services | Service | Port | Responsibility | |---|---|---| | `ha-gateway` | 50051 | gRPC boundary for Home Assistant — entity, light, and switch RPCs | | `ai-gateway` | 50052 | Intent extraction via Ollama, dispatches resolved actions to ha-gateway | | `discord-bot` | — | Slash-command registration and routing (`/light`, `/switch`, `/ai`) | ## Status Active and in daily use.