diff --git a/README.md b/README.md index 45dc4d5..b9d4f48 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,90 @@ # home-services -`home-services` is a Go mono-repo for internal home-control services built -around a gRPC gateway for Home Assistant. Discord and Alexa clients are meant -to talk to the gateway instead of integrating with Home Assistant directly. +`home-services` is a Go workspace for internal home-control services. It keeps +Home Assistant behind a gRPC gateway, adds an AI command gateway backed by +Ollama, and exposes both through a Discord slash-command bot. ## Architecture -The repo uses hexagonal architecture: +The services follow hexagonal architecture: -- the core domain is plain Go data and interfaces -- adapters at the edges handle gRPC, HTTP, Discord, telemetry, and logging -- dependencies point inward only +- `internal/core` contains domain types and ports +- `internal/app` contains use-case orchestration +- `internal/adapters` contains gRPC, Discord, Home Assistant, Ollama, logging, + and telemetry edges +- dependencies point inward toward the app and core packages -That means the business logic does not need to know about Discord, protobuf -transport details, or Home Assistant HTTP specifics. - -### Service relationship +Service flow: ```text -Discord bot ->\ - \ - -> ha-gateway -> Home Assistant - / -Alexa bridge ->/ +Discord users + | + v +discord-bot -----> ha-gateway -----> Home Assistant REST API + | + v +ai-gateway ------> Ollama + | + v +ha-gateway ``` -- `discord-bot` is an internal gRPC client of `ha-gateway` -- `alexa-bridge` is planned as another internal gRPC client of `ha-gateway` -- `ha-gateway` is the only service that talks to Home Assistant directly - -### Proto-first design - -All service contracts are defined in `proto/ha/v1/`. Generated Go code is -committed under `gen/` and used by both clients and servers. - -This gives every service the same: - -- RPC method names -- request and response message types -- protobuf serialization rules -- gRPC client/server bindings +The protobuf contracts live under `proto/`. Generated Go code is committed +under `gen/` and shared by all modules through the Go workspace. ## Services -### ha-gateway +### `ha-gateway` -`ha-gateway` is the single internal gRPC gateway to Home Assistant. It should -not be exposed publicly. +Internal gRPC gateway for Home Assistant. -- Port: `50051` -- Deployment model: internal-only, typically ClusterIP -- Implemented: - - `EntityService`: `GetState`, `ListStates` - - `LightService`: `TurnOn`, `TurnOff`, `Toggle`, `ListLights` - - `SwitchService`: `ListSwitches` -- Stubbed: - - `SwitchService`: `TurnOn`, `TurnOff`, `Toggle` - - `EventService` -- Observability: - - structured logging via `slog` - - `LOG_FORMAT=json` is suitable for production log pipelines - - `LOG_FORMAT=text` is more readable locally - - OpenTelemetry traces and metrics export over OTLP gRPC +- Default port: `50051` +- Talks to Home Assistant through the REST API +- Implements entity state lookup and discovery +- Implements light control and light discovery +- Implements switch discovery +- Stubs switch control and event streaming +- Supports optional mTLS when `TLS_DIR` is set +- Exposes gRPC health checks and reflection -Config: +See [ha-gateway/README.md](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ha-gateway/README.md). -| Variable | Default | Description | -| --- | --- | --- | -| `GRPC_PORT` | `50051` | gRPC listen port | -| `HA_BASE_URL` | empty | Base URL for Home Assistant | -| `HA_TOKEN` | none | Home Assistant long-lived access token | -| `OTEL_ENDPOINT` | empty | OTLP gRPC collector endpoint; empty disables telemetry | -| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error` | -| `LOG_FORMAT` | `json` | `json` or `text` | +### `ai-gateway` -Auth: +Internal gRPC gateway for AI-assisted home commands. -Inbound auth is not implemented yet. The current recommendation is mTLS between -internal services. A per-client API key interceptor is a possible alternative -for simpler environments. +- Default port: `50052` +- Calls Ollama for intent extraction and model listing +- Calls `ha-gateway` for light discovery and light actions +- Caches light discovery results for prompt context +- Supports optional mTLS when `TLS_DIR` is set +- Exposes gRPC health checks; reflection is enabled in debug logs -### discord-bot +See [ai-gateway/README.md](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ai-gateway/README.md). -`discord-bot` provides Discord slash commands for home control and discovery. +### `discord-bot` -- Connects to `ha-gateway` over internal gRPC -- Supports slash-command control for lights -- Supports list/discovery responses for lights and switches -- Observability: - - structured logging via `slog` - - OpenTelemetry traces and metrics via OTLP gRPC +Discord slash-command process for home control. -Config: +- Registers `/light`, `/switch`, and `/ai` commands +- Calls `ha-gateway` for direct Home Assistant commands +- Calls `ai-gateway` for free-form AI queries and model management +- Supports optional mTLS for gateway clients when `TLS_DIR` is set -| Variable | Default | Description | -| --- | --- | --- | -| `DISCORD_TOKEN` | none | Discord bot token | -| `GUILD_ID` | empty | Guild-scoped command registration target | -| `HA_GATEWAY_ADDR` | none | gRPC address of `ha-gateway` | -| `OTEL_ENDPOINT` | empty | OTLP gRPC collector endpoint; empty disables telemetry | -| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error` | -| `LOG_FORMAT` | `json` | `json` or `text` | - -Auth: - -No additional app-layer auth is implemented yet. The bot currently relies on -Discord authentication and the internal network boundary. - -### alexa-bridge - -`alexa-bridge` is a planned public HTTPS webhook service that will: - -- validate Alexa request signatures -- translate Alexa directives into gRPC calls to `ha-gateway` -- keep `ha-gateway` off the public internet - -Status: - -- stubbed concept only -- not implemented in this repo yet +See [discord-bot/README.md](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/README.md). ## Repo Structure ```text home-services/ -├── proto/ # Source of truth for protobuf service contracts -│ └── ha/v1/ -├── gen/ # Committed generated Go protobuf/gRPC code; do not edit -├── ha-gateway/ -│ ├── cmd/gateway/ # Process entrypoint and startup wiring -│ └── internal/ -│ ├── core/ # Pure domain and port definitions, no framework logic -│ ├── app/ # Orchestration between ports -│ ├── adapters/ # gRPC handlers, HA client, and other I/O -│ ├── logger/ # Context-aware slog helpers -│ └── telemetry/ # OpenTelemetry setup and shutdown -├── discord-bot/ -│ ├── cmd/bot/ # Process entrypoint and Discord session wiring -│ └── internal/ -│ ├── app/ # Command orchestration and formatting -│ ├── adapters/ # Discord interaction handlers and gRPC client -│ ├── core/ports/driven/ # App-facing ha-gateway contract -│ ├── logger/ # Context-aware slog helpers -│ └── telemetry/ # OpenTelemetry setup and shutdown -├── buf.yaml # Buf module config -├── buf.gen.yaml # Buf generation config -└── go.work # Go workspace for local module development +├── ai-gateway/ # AI gRPC gateway backed by Ollama and ha-gateway +├── discord-bot/ # Discord slash-command bot +├── gen/ # Committed generated protobuf/gRPC Go code +├── ha-gateway/ # Home Assistant gRPC gateway +├── proto/ # Source protobuf contracts +├── buf.gen.yaml # Buf generation config +├── buf.yaml # Buf module config +└── go.work # Go workspace ``` ## Local Development @@ -153,58 +92,97 @@ home-services/ Prerequisites: - Go `1.26+` -- `buf` -- `grpcurl` -- a running Home Assistant instance +- `buf`, when changing `.proto` files +- `grpcurl`, for manual gRPC checks +- Home Assistant with a long-lived access token +- Ollama, if running AI queries +- A Discord application and bot token, if running `discord-bot` -Setup: +Set up dependencies: ```bash -# Clone and set up workspace -git clone https://gitea.nik4nao.com/nik/home-services -cd home-services go work sync - -# Regenerate proto (only needed if .proto files change) -buf generate - -# Configure ha-gateway -cp ha-gateway/.env.example ha-gateway/.env -# Edit ha-gateway/.env — set HA_BASE_URL and HA_TOKEN - -# Run ha-gateway -cd ha-gateway && go run ./cmd/gateway - -# Run discord-bot (separate terminal) -cd discord-bot && go run ./cmd/bot ``` -Smoke testing with `grpcurl`: +Regenerate protobuf code after changing files under `proto/`: + +```bash +buf generate +``` + +Run services in separate terminals: + +```bash +cp ha-gateway/.env.example ha-gateway/.env +cd ha-gateway +go run ./cmd/gateway +``` + +```bash +cp ai-gateway/.env.example ai-gateway/.env +cd ai-gateway +go run ./cmd/gateway +``` + +```bash +cp discord-bot/.env.example discord-bot/.env +cd discord-bot +go run ./cmd/bot +``` + +Each service loads `.env` from its current working directory, so run commands +from the service directory when using the example env files. + +## Test And Build + +Run tests from the workspace root: + +```bash +go test ./ha-gateway/... ./ai-gateway/... ./discord-bot/... +``` + +Build service binaries from the workspace root: + +```bash +go build ./ha-gateway/... ./ai-gateway/... ./discord-bot/... +``` + +Build container images from the workspace root: + +```bash +docker build -f ha-gateway/Dockerfile -t ha-gateway:dev . +docker build -f ai-gateway/Dockerfile -t ai-gateway:dev . +docker build -f discord-bot/Dockerfile -t discord-bot:dev . +``` + +## gRPC Smoke Checks + +With `ha-gateway` running locally: ```bash -# List all lights grpcurl -plaintext -d '{"domain":"light"}' \ localhost:50051 ha.v1.EntityService/ListStates -# Turn on a light grpcurl -plaintext -d '{"entity_id":"light.living_room","brightness_pct":80}' \ localhost:50051 ha.v1.LightService/TurnOn ``` -## Building +With `ai-gateway` running locally: ```bash -# Build ha-gateway image (run from repo root) -docker build -f ha-gateway/Dockerfile -t ha-gateway:dev . +grpcurl -plaintext -d '{"text":"turn on the desk lamp","source":"local"}' \ + localhost:50052 ai.v1.AIService/Query -# Build discord-bot image -docker build -f discord-bot/Dockerfile -t discord-bot:dev . +grpcurl -plaintext -d '{}' localhost:50052 ai.v1.AIService/ListModels ``` -## What's Next +## Configuration Notes -- Auth: mTLS between services using an internal cert-manager CA -- SwitchService control implementation -- EventService with Home Assistant WebSocket fan-out broker -- `alexa-bridge` implementation -- Gitea CI pipeline for automated build and push +- `LOG_FORMAT=json` is the production default; `LOG_FORMAT=text` is easier + locally. +- `OTEL_ENDPOINT` enables OTLP gRPC traces and metrics. Leave it empty for + local no-op telemetry. +- `TLS_DIR` enables mTLS. The directory must contain `tls.crt`, `tls.key`, and + `ca.crt`. +- Inbound app-layer authorization is not implemented. Keep the gateways on a + trusted internal network or use mTLS. diff --git a/ai-gateway/README.md b/ai-gateway/README.md new file mode 100644 index 0000000..3c9bf14 --- /dev/null +++ b/ai-gateway/README.md @@ -0,0 +1,126 @@ +# ai-gateway + +`ai-gateway` is an internal gRPC service that turns free-form text into home +assistant actions. It asks Ollama to produce a structured intent, resolves that +intent against known Home Assistant lights, and calls `ha-gateway` for any +approved action. + +## Runtime Flow + +1. The service loads `.env`, configures logging and telemetry, and starts gRPC + on `GRPC_PORT`. +2. `AIService.Query` receives prompt text and an optional model name. +3. The app refreshes or reads the light cache from `ha-gateway`. +4. Ollama receives a prompt containing the user text and known lights. +5. The app parses the model output as JSON intent data. +6. Supported intents are executed through `ha-gateway`; unsupported or invalid + responses return a plain explanation without taking action. + +## gRPC API + +Defined in [proto/ai/v1/ai.proto](https://gitea.nik4nao.com/nik/home-services/src/branch/main/proto/ai/v1/ai.proto). + +- `AIService.Query`: sends text to the AI command flow +- `AIService.ListModels`: returns model names reported by Ollama + +Supported action intents currently focus on lights: + +- turn on a light +- turn off a light +- list known lights +- no-op / unknown intent + +## Configuration + +Environment variables: + +| Variable | Default | Description | +| --- | --- | --- | +| `GRPC_PORT` | `50052` | gRPC listen port | +| `OLLAMA_URL` | `http://192.168.7.96:11434` | Ollama base URL | +| `OLLAMA_MODEL` | `llama3` | Default model for queries without an explicit model | +| `OLLAMA_TIMEOUT` | `120s` | HTTP timeout for Ollama calls | +| `HA_GATEWAY_ADDR` | `ha-gateway.home-services.svc.cluster.local:50051` | gRPC address for `ha-gateway` | +| `HA_GATEWAY_SERVER_NAME` | `ha-gateway.home-services.svc.cluster.local` | Expected server name for TLS | +| `TLS_DIR` | empty | Enables mTLS for the server and gateway client when set | +| `OTEL_ENDPOINT` | empty | OTLP gRPC collector endpoint; empty disables telemetry | +| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error` | +| `LOG_FORMAT` | `json` | `json` or `text` | +| `LIGHT_CACHE_TTL` | `60s` | Light discovery cache lifetime | + +Example env file: [.env.example](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ai-gateway/.env.example) + +When `TLS_DIR` is set, the directory must contain `tls.crt`, `tls.key`, and +`ca.crt`. + +## Local Run + +Start `ha-gateway` first, then run: + +```bash +cp .env.example .env +go run ./cmd/gateway +``` + +Run from `ai-gateway/` so `godotenv` loads `ai-gateway/.env`. + +For local plaintext development: + +```text +HA_GATEWAY_ADDR=localhost:50051 +TLS_DIR= +``` + +## Smoke Checks + +```bash +grpcurl -plaintext -d '{"text":"list the lights","source":"local"}' \ + localhost:50052 ai.v1.AIService/Query + +grpcurl -plaintext -d '{}' localhost:50052 ai.v1.AIService/ListModels +``` + +Reflection is only registered when `LOG_LEVEL=debug`; otherwise call by using +the compiled proto descriptors or generated clients. + +## Test And Build + +```bash +go test ./... +go build ./... +``` + +Build the container image from the workspace root: + +```bash +docker build -f ai-gateway/Dockerfile -t ai-gateway:dev . +``` + +Optionally pass a build version: + +```bash +docker build -f ai-gateway/Dockerfile --build-arg VERSION=$(git rev-parse --short HEAD) -t ai-gateway:dev . +``` + +## Package Map + +```text +cmd/gateway/ # process entrypoint and wiring +internal/adapters/primary/grpc/ # AIService gRPC server +internal/adapters/secondary/ollama/ # Ollama HTTP client +internal/adapters/secondary/hagateway/ # ha-gateway gRPC client +internal/app/ # query orchestration and intent dispatch +internal/config/ # environment loading +internal/core/domain/ # prompt, intent, and cache types +internal/core/ports/driven/ # app-facing LLM and HA interfaces +internal/logger/ # slog setup +internal/telemetry/ # OpenTelemetry setup +``` + +## Limitations + +- Intent parsing depends on the selected model returning valid JSON. +- Light actions are supported; broader Home Assistant domains are not wired yet. +- The light cache is per-process memory and refreshes by TTL. +- Keep this service internal or protect it with mTLS; it does not implement + separate app-layer authorization. diff --git a/discord-bot/README.md b/discord-bot/README.md index 9a3abf5..8d493da 100644 --- a/discord-bot/README.md +++ b/discord-bot/README.md @@ -1,159 +1,132 @@ # discord-bot -`discord-bot` is a Discord slash-command service that talks to -`ha-gateway` over plaintext gRPC and exposes common Home Assistant actions in -Discord. +`discord-bot` is a Discord slash-command service for home control. It calls +`ha-gateway` for direct Home Assistant actions and `ai-gateway` for free-form +AI-assisted commands. -## How It Works +## Runtime Flow -Runtime flow: +1. The process loads `.env`, opens a Discord session, and registers slash + commands. +2. A user runs a command such as `/light list`, `/light on`, or `/ai query`. +3. The Discord adapter validates and routes the interaction to the app layer. +4. The app layer calls `ha-gateway` or `ai-gateway` through secondary gRPC + adapters. +5. The bot sends a Discord response. Long-running AI work is tracked so + shutdown can wait briefly for in-flight requests. -1. The bot starts a Discord session and registers slash commands. -2. A user runs a slash command such as `/light list` or `/light on`. -3. The Discord adapter routes the interaction to the app layer. -4. The app layer calls `ha-gateway` through the secondary gRPC adapter. -5. `ha-gateway` talks to Home Assistant and returns the result. -6. The bot formats the response back into a Discord message. +## Commands -The service follows the same hexagonal structure as `ha-gateway`: - -- `cmd/bot`: process bootstrap -- `internal/adapters/primary/discord`: slash command registration and handlers -- `internal/app`: command orchestration and response formatting -- `internal/adapters/secondary/gateway`: gRPC client for `ha-gateway` -- `internal/core/ports/driven`: app-facing gateway interface -- `internal/config`: env loading -- `internal/telemetry`: OpenTelemetry setup - -## Command Behavior - -- List commands respond as regular channel messages. -- Action commands use deferred ephemeral replies, then send a follow-up result. -- Light autocomplete is backed by `ListLights` from `ha-gateway`. -- Switch autocomplete support exists in code, but there is currently no switch - action command that uses it. - -## Common Commands - -### List lights +### Lights ```text /light list -``` - -Shows all discovered lights in a fixed-width block. Each line includes: - -- state emoji: `🟢` on, `🔴` off, `⚠️` unavailable or other state -- friendly name -- raw state -- supported color modes -- kelvin range when available - -Example output shape: - -```text -🟢 Desk Lamp on color_temp,xy 2000-6535K -🔴 Closet off hs,color_temp 2202-4000K -⚠️ Hall Group unavailable color_temp 2000-6535K hue-group -``` - -### Turn on a light - -```text /light on light: brightness:80 color_temp:3000 -``` - -Options: - -- `light`: required, autocomplete enabled -- `brightness`: optional, `1-100` -- `color_temp`: optional, `2000-6535` - -The bot responds with a confirmation such as: - -```text -Turned on `Desk Lamp` (brightness 80%, 3000K). -``` - -### Turn off a light - -```text /light off light: transition:3 -``` - -Options: - -- `light`: required, autocomplete enabled -- `transition`: optional, `0-30` seconds - -### Toggle a light - -```text /light toggle light: ``` -Option: +- `light` is required for action commands and uses autocomplete. +- `brightness` is optional and accepts `1-100`. +- `color_temp` is optional and accepts `2000-6535` kelvin. +- `transition` is optional and accepts `0-30` seconds. -- `light`: required, autocomplete enabled - -### List switches +### Switches ```text /switch list ``` -Shows discovered switches in a fixed-width block with: +Switch control commands are not exposed yet. `ha-gateway` currently only +implements switch discovery. -- state emoji -- friendly name -- raw state -- device class +### AI + +```text +/ai query text: +/ai model list +/ai model get +/ai model set name: +``` + +AI queries are sent to `ai-gateway`. The active model is stored in process +memory, so it resets when the bot restarts. ## Configuration Environment variables: -- `DISCORD_TOKEN`: required Discord bot token -- `GUILD_ID`: optional guild ID; if set, commands are registered to that guild -- `HA_GATEWAY_ADDR`: required gRPC address for `ha-gateway` -- `OTEL_ENDPOINT`: optional OTLP gRPC endpoint; empty disables telemetry +| Variable | Default | Description | +| --- | --- | --- | +| `DISCORD_TOKEN` | required | Discord bot token | +| `GUILD_ID` | empty | Guild-scoped command registration target; empty registers global commands | +| `HA_GATEWAY_ADDR` | required | gRPC address for `ha-gateway` | +| `AI_GATEWAY_ADDR` | `ai-gateway.home-services.svc.cluster.local:50052` | gRPC address for `ai-gateway` | +| `TLS_DIR` | empty | Enables mTLS for gateway clients when set | +| `OTEL_ENDPOINT` | empty | OTLP gRPC collector endpoint; empty disables telemetry | +| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error` | +| `LOG_FORMAT` | `json` | `json` or `text` | -Example env file: [`.env.example`](/Users/nik-macbookair/repo/home-service/discord-bot/.env.example) +Example env file: [.env.example](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/.env.example) + +When `TLS_DIR` is set, the directory must contain `tls.crt`, `tls.key`, and +`ca.crt`. ## Local Run -Create an env file and fill in real values: - ```bash -cp discord-bot/.env.example discord-bot/.env -``` - -Then run: - -```bash -cd discord-bot +cp .env.example .env go run ./cmd/bot ``` -## Build +Run from `discord-bot/` so `godotenv` loads `discord-bot/.env`. + +For local plaintext gateway connections, leave `TLS_DIR` empty and use: + +```text +HA_GATEWAY_ADDR=localhost:50051 +AI_GATEWAY_ADDR=localhost:50052 +``` + +## Test And Build ```bash -cd discord-bot +go test ./... go build ./... ``` -## Docker - -Build from the repo root: +Build the container image from the workspace root: ```bash docker build -f discord-bot/Dockerfile -t discord-bot:dev . ``` -## Current Limitations +Optionally pass a build version: -- no switch on/off/toggle command is exposed yet -- the bot depends on `ha-gateway` being reachable and healthy -- list formatting is optimized for monospace code blocks, not rich embeds -- authentication/authorization is whatever Discord and the internal network - provide; the bot does not add another auth layer itself +```bash +docker build -f discord-bot/Dockerfile --build-arg VERSION=$(git rev-parse --short HEAD) -t discord-bot:dev . +``` + +## Package Map + +```text +cmd/bot/ # process entrypoint and wiring +internal/adapters/primary/discord/ # slash command registration and handlers +internal/adapters/secondary/gateway/ # ha-gateway gRPC client +internal/adapters/secondary/aigateway/ # ai-gateway gRPC client +internal/app/ # command orchestration and formatting +internal/config/ # environment loading +internal/core/ports/driven/ # app-facing gateway interfaces +internal/modelstore/ # in-memory active AI model store +internal/modelvalidator/ # model availability checks +internal/logger/ # slog setup +internal/telemetry/ # OpenTelemetry setup +``` + +## Limitations + +- Switch commands are discovery-only. +- The active AI model is not persisted. +- The bot relies on Discord auth plus internal gateway/network controls; it + does not implement per-user authorization. +- List output is optimized for monospace Discord messages, not rich embeds. diff --git a/ha-gateway/README.md b/ha-gateway/README.md new file mode 100644 index 0000000..ab510c5 --- /dev/null +++ b/ha-gateway/README.md @@ -0,0 +1,129 @@ +# ha-gateway + +`ha-gateway` is the internal gRPC boundary for Home Assistant. It exposes a +small protobuf API to other home services and keeps Home Assistant tokens and +REST details out of clients. + +## Runtime Flow + +1. The service loads `.env`, configures logging and telemetry, and starts gRPC + on `GRPC_PORT`. +2. App services are wired to a Home Assistant REST adapter. +3. Light and switch discovery caches are refreshed during startup when possible. +4. gRPC clients call entity, light, switch, or event services. +5. The adapter maps requests to Home Assistant REST state and service calls. + +## gRPC API + +Contracts are defined under [proto/ha/v1](https://gitea.nik4nao.com/nik/home-services/src/branch/main/proto/ha/v1). + +Implemented: + +- `EntityService.GetState` +- `EntityService.ListStates` +- `LightService.TurnOn` +- `LightService.TurnOff` +- `LightService.Toggle` +- `LightService.ListLights` +- `SwitchService.ListSwitches` + +Stubbed: + +- `SwitchService.TurnOn` +- `SwitchService.TurnOff` +- `SwitchService.Toggle` +- `EventService` + +The server also registers gRPC health checks and reflection. + +## Configuration + +Environment variables: + +| Variable | Default | Description | +| --- | --- | --- | +| `GRPC_PORT` | `50051` | gRPC listen port | +| `HA_BASE_URL` | empty | Home Assistant base URL, for example `http://ha.home.arpa:8123` | +| `HA_TOKEN` | required | Home Assistant long-lived access token | +| `TLS_DIR` | empty | Enables mTLS for the gRPC server when set | +| `OTEL_ENDPOINT` | empty | OTLP gRPC collector endpoint; empty disables telemetry | +| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, or `error` | +| `LOG_FORMAT` | `json` | `json` or `text` | + +Example env file: [.env.example](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ha-gateway/.env.example) + +When `TLS_DIR` is set, the directory must contain `tls.crt`, `tls.key`, and +`ca.crt`. + +## Local Run + +```bash +cp .env.example .env +go run ./cmd/gateway +``` + +Run from `ha-gateway/` so `godotenv` loads `ha-gateway/.env`. + +For local plaintext development: + +```text +GRPC_PORT=50051 +HA_BASE_URL=http://ha.home.arpa:8123 +HA_TOKEN= +TLS_DIR= +``` + +## Smoke Checks + +```bash +grpcurl -plaintext -d '{"domain":"light"}' \ + localhost:50051 ha.v1.EntityService/ListStates + +grpcurl -plaintext -d '{}' localhost:50051 ha.v1.LightService/ListLights + +grpcurl -plaintext -d '{"entity_id":"light.living_room","brightness_pct":80}' \ + localhost:50051 ha.v1.LightService/TurnOn + +grpcurl -plaintext -d '{}' localhost:50051 grpc.health.v1.Health/Check +``` + +## Test And Build + +```bash +go test ./... +go build ./... +``` + +Build the container image from the workspace root: + +```bash +docker build -f ha-gateway/Dockerfile -t ha-gateway:dev . +``` + +Optionally pass a build version: + +```bash +docker build -f ha-gateway/Dockerfile --build-arg VERSION=$(git rev-parse --short HEAD) -t ha-gateway:dev . +``` + +## Package Map + +```text +cmd/gateway/ # process entrypoint and wiring +internal/adapters/primary/grpc/ # gRPC service implementations +internal/adapters/secondary/ha/ # Home Assistant REST adapter +internal/app/ # entity, light, and switch orchestration +internal/config/ # environment loading +internal/core/domain/ # domain types +internal/core/ports/ # driving and driven interfaces +internal/logger/ # slog setup +internal/telemetry/ # OpenTelemetry setup +``` + +## Limitations + +- Switch control RPCs return not implemented. +- `EventService` is registered but not implemented. +- Home Assistant WebSocket event streaming is still a TODO. +- Keep this service internal or protect it with mTLS; it does not implement + separate app-layer authorization.