feat: update README files for ha-gateway and ai-gateway, and enhance discord-bot documentation
This commit is contained in:
parent
88c9a77a58
commit
685b6e2054
280
README.md
280
README.md
@ -1,151 +1,90 @@
|
|||||||
# home-services
|
# home-services
|
||||||
|
|
||||||
`home-services` is a Go mono-repo for internal home-control services built
|
`home-services` is a Go workspace for internal home-control services. It keeps
|
||||||
around a gRPC gateway for Home Assistant. Discord and Alexa clients are meant
|
Home Assistant behind a gRPC gateway, adds an AI command gateway backed by
|
||||||
to talk to the gateway instead of integrating with Home Assistant directly.
|
Ollama, and exposes both through a Discord slash-command bot.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
The repo uses hexagonal architecture:
|
The services follow hexagonal architecture:
|
||||||
|
|
||||||
- the core domain is plain Go data and interfaces
|
- `internal/core` contains domain types and ports
|
||||||
- adapters at the edges handle gRPC, HTTP, Discord, telemetry, and logging
|
- `internal/app` contains use-case orchestration
|
||||||
- dependencies point inward only
|
- `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
|
Service flow:
|
||||||
transport details, or Home Assistant HTTP specifics.
|
|
||||||
|
|
||||||
### Service relationship
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Discord bot ->\
|
Discord users
|
||||||
\
|
|
|
||||||
-> ha-gateway -> Home Assistant
|
v
|
||||||
/
|
discord-bot -----> ha-gateway -----> Home Assistant REST API
|
||||||
Alexa bridge ->/
|
|
|
||||||
|
v
|
||||||
|
ai-gateway ------> Ollama
|
||||||
|
|
|
||||||
|
v
|
||||||
|
ha-gateway
|
||||||
```
|
```
|
||||||
|
|
||||||
- `discord-bot` is an internal gRPC client of `ha-gateway`
|
The protobuf contracts live under `proto/`. Generated Go code is committed
|
||||||
- `alexa-bridge` is planned as another internal gRPC client of `ha-gateway`
|
under `gen/` and shared by all modules through the Go workspace.
|
||||||
- `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
|
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
### ha-gateway
|
### `ha-gateway`
|
||||||
|
|
||||||
`ha-gateway` is the single internal gRPC gateway to Home Assistant. It should
|
Internal gRPC gateway for Home Assistant.
|
||||||
not be exposed publicly.
|
|
||||||
|
|
||||||
- Port: `50051`
|
- Default port: `50051`
|
||||||
- Deployment model: internal-only, typically ClusterIP
|
- Talks to Home Assistant through the REST API
|
||||||
- Implemented:
|
- Implements entity state lookup and discovery
|
||||||
- `EntityService`: `GetState`, `ListStates`
|
- Implements light control and light discovery
|
||||||
- `LightService`: `TurnOn`, `TurnOff`, `Toggle`, `ListLights`
|
- Implements switch discovery
|
||||||
- `SwitchService`: `ListSwitches`
|
- Stubs switch control and event streaming
|
||||||
- Stubbed:
|
- Supports optional mTLS when `TLS_DIR` is set
|
||||||
- `SwitchService`: `TurnOn`, `TurnOff`, `Toggle`
|
- Exposes gRPC health checks and reflection
|
||||||
- `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
|
|
||||||
|
|
||||||
Config:
|
See [ha-gateway/README.md](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ha-gateway/README.md).
|
||||||
|
|
||||||
| Variable | Default | Description |
|
### `ai-gateway`
|
||||||
| --- | --- | --- |
|
|
||||||
| `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` |
|
|
||||||
|
|
||||||
Auth:
|
Internal gRPC gateway for AI-assisted home commands.
|
||||||
|
|
||||||
Inbound auth is not implemented yet. The current recommendation is mTLS between
|
- Default port: `50052`
|
||||||
internal services. A per-client API key interceptor is a possible alternative
|
- Calls Ollama for intent extraction and model listing
|
||||||
for simpler environments.
|
- 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
|
Discord slash-command process for home control.
|
||||||
- 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
|
|
||||||
|
|
||||||
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 |
|
See [discord-bot/README.md](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/README.md).
|
||||||
| --- | --- | --- |
|
|
||||||
| `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
|
|
||||||
|
|
||||||
## Repo Structure
|
## Repo Structure
|
||||||
|
|
||||||
```text
|
```text
|
||||||
home-services/
|
home-services/
|
||||||
├── proto/ # Source of truth for protobuf service contracts
|
├── ai-gateway/ # AI gRPC gateway backed by Ollama and ha-gateway
|
||||||
│ └── ha/v1/
|
├── discord-bot/ # Discord slash-command bot
|
||||||
├── gen/ # Committed generated Go protobuf/gRPC code; do not edit
|
├── gen/ # Committed generated protobuf/gRPC Go code
|
||||||
├── ha-gateway/
|
├── ha-gateway/ # Home Assistant gRPC gateway
|
||||||
│ ├── cmd/gateway/ # Process entrypoint and startup wiring
|
├── proto/ # Source protobuf contracts
|
||||||
│ └── 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
|
├── buf.gen.yaml # Buf generation config
|
||||||
└── go.work # Go workspace for local module development
|
├── buf.yaml # Buf module config
|
||||||
|
└── go.work # Go workspace
|
||||||
```
|
```
|
||||||
|
|
||||||
## Local Development
|
## Local Development
|
||||||
@ -153,58 +92,97 @@ home-services/
|
|||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
- Go `1.26+`
|
- Go `1.26+`
|
||||||
- `buf`
|
- `buf`, when changing `.proto` files
|
||||||
- `grpcurl`
|
- `grpcurl`, for manual gRPC checks
|
||||||
- a running Home Assistant instance
|
- 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
|
```bash
|
||||||
# Clone and set up workspace
|
|
||||||
git clone https://gitea.nik4nao.com/nik/home-services
|
|
||||||
cd home-services
|
|
||||||
go work sync
|
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
|
```bash
|
||||||
# List all lights
|
|
||||||
grpcurl -plaintext -d '{"domain":"light"}' \
|
grpcurl -plaintext -d '{"domain":"light"}' \
|
||||||
localhost:50051 ha.v1.EntityService/ListStates
|
localhost:50051 ha.v1.EntityService/ListStates
|
||||||
|
|
||||||
# Turn on a light
|
|
||||||
grpcurl -plaintext -d '{"entity_id":"light.living_room","brightness_pct":80}' \
|
grpcurl -plaintext -d '{"entity_id":"light.living_room","brightness_pct":80}' \
|
||||||
localhost:50051 ha.v1.LightService/TurnOn
|
localhost:50051 ha.v1.LightService/TurnOn
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
With `ai-gateway` running locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build ha-gateway image (run from repo root)
|
grpcurl -plaintext -d '{"text":"turn on the desk lamp","source":"local"}' \
|
||||||
docker build -f ha-gateway/Dockerfile -t ha-gateway:dev .
|
localhost:50052 ai.v1.AIService/Query
|
||||||
|
|
||||||
# Build discord-bot image
|
grpcurl -plaintext -d '{}' localhost:50052 ai.v1.AIService/ListModels
|
||||||
docker build -f discord-bot/Dockerfile -t discord-bot:dev .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## What's Next
|
## Configuration Notes
|
||||||
|
|
||||||
- Auth: mTLS between services using an internal cert-manager CA
|
- `LOG_FORMAT=json` is the production default; `LOG_FORMAT=text` is easier
|
||||||
- SwitchService control implementation
|
locally.
|
||||||
- EventService with Home Assistant WebSocket fan-out broker
|
- `OTEL_ENDPOINT` enables OTLP gRPC traces and metrics. Leave it empty for
|
||||||
- `alexa-bridge` implementation
|
local no-op telemetry.
|
||||||
- Gitea CI pipeline for automated build and push
|
- `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.
|
||||||
|
|||||||
126
ai-gateway/README.md
Normal file
126
ai-gateway/README.md
Normal file
@ -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.
|
||||||
@ -1,159 +1,132 @@
|
|||||||
# discord-bot
|
# discord-bot
|
||||||
|
|
||||||
`discord-bot` is a Discord slash-command service that talks to
|
`discord-bot` is a Discord slash-command service for home control. It calls
|
||||||
`ha-gateway` over plaintext gRPC and exposes common Home Assistant actions in
|
`ha-gateway` for direct Home Assistant actions and `ai-gateway` for free-form
|
||||||
Discord.
|
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.
|
## 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.
|
|
||||||
|
|
||||||
The service follows the same hexagonal structure as `ha-gateway`:
|
### Lights
|
||||||
|
|
||||||
- `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
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/light list
|
/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:<entity> brightness:80 color_temp:3000
|
/light on light:<entity> 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:<entity> transition:3
|
/light off light:<entity> transition:3
|
||||||
```
|
|
||||||
|
|
||||||
Options:
|
|
||||||
|
|
||||||
- `light`: required, autocomplete enabled
|
|
||||||
- `transition`: optional, `0-30` seconds
|
|
||||||
|
|
||||||
### Toggle a light
|
|
||||||
|
|
||||||
```text
|
|
||||||
/light toggle light:<entity>
|
/light toggle light:<entity>
|
||||||
```
|
```
|
||||||
|
|
||||||
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
|
### Switches
|
||||||
|
|
||||||
### List switches
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/switch list
|
/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
|
### AI
|
||||||
- friendly name
|
|
||||||
- raw state
|
```text
|
||||||
- device class
|
/ai query text:<prompt>
|
||||||
|
/ai model list
|
||||||
|
/ai model get
|
||||||
|
/ai model set name:<model>
|
||||||
|
```
|
||||||
|
|
||||||
|
AI queries are sent to `ai-gateway`. The active model is stored in process
|
||||||
|
memory, so it resets when the bot restarts.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Environment variables:
|
Environment variables:
|
||||||
|
|
||||||
- `DISCORD_TOKEN`: required Discord bot token
|
| Variable | Default | Description |
|
||||||
- `GUILD_ID`: optional guild ID; if set, commands are registered to that guild
|
| --- | --- | --- |
|
||||||
- `HA_GATEWAY_ADDR`: required gRPC address for `ha-gateway`
|
| `DISCORD_TOKEN` | required | Discord bot token |
|
||||||
- `OTEL_ENDPOINT`: optional OTLP gRPC endpoint; empty disables telemetry
|
| `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
|
## Local Run
|
||||||
|
|
||||||
Create an env file and fill in real values:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp discord-bot/.env.example discord-bot/.env
|
cp .env.example .env
|
||||||
```
|
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd discord-bot
|
|
||||||
go run ./cmd/bot
|
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
|
```bash
|
||||||
cd discord-bot
|
go test ./...
|
||||||
go build ./...
|
go build ./...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Docker
|
Build the container image from the workspace root:
|
||||||
|
|
||||||
Build from the repo root:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f discord-bot/Dockerfile -t discord-bot:dev .
|
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
|
```bash
|
||||||
- the bot depends on `ha-gateway` being reachable and healthy
|
docker build -f discord-bot/Dockerfile --build-arg VERSION=$(git rev-parse --short HEAD) -t discord-bot:dev .
|
||||||
- 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
|
## 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.
|
||||||
|
|||||||
129
ha-gateway/README.md
Normal file
129
ha-gateway/README.md
Normal file
@ -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=<long-lived-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.
|
||||||
Loading…
x
Reference in New Issue
Block a user