# home-service This repo is a small Home Assistant control platform built around gRPC. There are three important pieces: - `proto/`: the API contract, written in protobuf - `gen/`: generated Go code from that contract - `ha-gateway/`: the gRPC server that talks to Home Assistant - `discord-bot/`: a Discord client that talks to `ha-gateway` If you are new to gRPC, the most important idea is: 1. define the API once in `.proto` files 2. generate client/server code from that definition 3. let all services use the same generated types so they agree on request and response shape ## What Each Folder Does ```text . ├── proto/ # Source protobuf service and message definitions ├── gen/ # Generated Go code from proto/ ├── ha-gateway/ # gRPC server + Home Assistant REST adapter ├── discord-bot/ # Discord slash-command app + gRPC client to ha-gateway ├── buf.yaml # Buf module config ├── buf.gen.yaml # Buf code generation config └── go.work # Go workspace linking all local modules ``` ## Why `gen/` Exists `gen/` is the bridge between your `.proto` files and your Go services. The `.proto` files in `proto/ha/v1/` are the source of truth. They define: - service names like `LightService` and `SwitchService` - RPC methods like `ListLights`, `TurnOn`, and `ListSwitches` - message schemas like `TurnOnRequest`, `LightEntity`, and `SwitchEntity` Those `.proto` files are not directly used by Go at runtime. They are compiled into Go source files in `gen/ha/v1/`, such as: - `light.pb.go` - `light_grpc.pb.go` - `switch.pb.go` - `switch_grpc.pb.go` That generated code gives you: - Go structs for protobuf messages - gRPC client interfaces for callers - gRPC server interfaces and registration helpers for servers In this repo: - `discord-bot` imports `gitea.nik4nao.com/nik/home-services/gen/ha/v1` to get generated gRPC clients like `hav1.LightServiceClient` - `ha-gateway` imports the same package to register generated gRPC servers like `hav1.RegisterLightServiceServer(...)` Without `gen/`, both services would need to hand-write and manually keep in sync: - request/response structs - method names - serialization logic - client/server glue That is exactly what gRPC code generation is meant to remove. ## High-Level Data Flow The request flow for a Discord command looks like this: ```text Discord user -> discord-bot primary adapter -> discord-bot app layer -> discord-bot gRPC client adapter -> generated gRPC client code from gen/ -> network -> generated gRPC server code from gen/ -> ha-gateway gRPC adapter -> ha-gateway app layer -> ha-gateway Home Assistant REST adapter -> Home Assistant ``` And then the response goes back in reverse. ## Concrete Example: `/light on` Here is the real request path in this repo when a user runs `/light on` in Discord. ### 1. Discord receives the slash command `discord-bot` opens a Discord session in [main.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/cmd/bot/main.go) and registers a handler from [handler.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/internal/adapters/primary/discord/handler.go). When the user runs: ```text /light on light:light.desk brightness:80 color_temp:3000 ``` Discord sends an interaction event to the bot. ### 2. The Discord adapter routes the command In [handler.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/internal/adapters/primary/discord/handler.go), the command is routed by command name and subcommand: - `light.on` goes to `HandleLightOn(...)` This is still Discord-specific code. It knows about slash-command options and ephemeral responses. ### 3. The app layer turns the command into an internal use case In [command.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/internal/app/command.go), `HandleLightOn(...)` does two things: - looks up a friendly light name for the confirmation message - calls the driven port `TurnOnLight(...)` This layer does not know protobuf details. It only knows the app wants to turn on a light. ### 4. The secondary adapter turns that use case into a gRPC request In [client.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/internal/adapters/secondary/gateway/client.go), `TurnOnLight(...)` creates: - a generated protobuf request: `hav1.TurnOnRequest` - and sends it with a generated gRPC client: `hav1.LightServiceClient` This is the exact place where `discord-bot` depends on `gen/`. Why? Because `hav1.TurnOnRequest` and `hav1.LightServiceClient` are generated from `proto/ha/v1/light.proto`. ### 5. gRPC sends the protobuf message over the network The generated client serializes the request into protobuf bytes and sends it to `ha-gateway` over gRPC. At this point: - `discord-bot` does not know anything about Home Assistant REST - it only knows the gRPC contract defined in `proto/` ### 6. `ha-gateway` receives the RPC In [main.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ha-gateway/cmd/gateway/main.go), `ha-gateway` registers the generated server bindings: - `hav1.RegisterLightServiceServer(...)` - `hav1.RegisterSwitchServiceServer(...)` - `hav1.RegisterEntityServiceServer(...)` The generated gRPC server code from `gen/` receives the network request and dispatches it to the real handler implementation in [light.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ha-gateway/internal/adapters/primary/grpc/light.go). This is the exact mirror image of the client side: - `discord-bot` uses generated client code - `ha-gateway` uses generated server code ### 7. The gRPC adapter maps protobuf types into domain input In [light.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ha-gateway/internal/adapters/primary/grpc/light.go), the handler receives `*hav1.TurnOnRequest`. It converts that protobuf message into domain parameters and calls the app service. This separation matters because it keeps the core app logic from depending on protobuf or transport-specific concerns. ### 8. The app layer decides what Home Assistant call to make In `ha-gateway/internal/app/light.go`, the app layer builds a service payload such as: ```json { "entity_id": "light.desk", "brightness_pct": 80, "color_temp_kelvin": 3000 } ``` Then it calls the driven Home Assistant client. ### 9. The Home Assistant adapter performs the REST call In [client.go](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ha-gateway/internal/adapters/secondary/ha/client.go), `CallService(...)` sends an HTTP request to Home Assistant: - `POST /api/services/light/turn_on` using the Home Assistant base URL and token from env config. ### 10. The response comes back up the stack Then the flow reverses: 1. Home Assistant returns JSON 2. `ha-gateway` maps it into internal state 3. `ha-gateway` maps internal state into generated protobuf response types 4. generated gRPC server code serializes the response 5. generated gRPC client code in `discord-bot` deserializes it 6. `discord-bot` app layer formats a human response like: `Turned on 'Desk Lamp' (brightness 80%, 3000K).` 7. the Discord adapter sends that message back to Discord ## Why This Is Split Into Multiple Layers If you are new to gRPC architecture, it can feel like there are too many steps. Each layer has a specific job: - `proto/`: defines the API contract - `gen/`: generated transport code from the contract - `discord-bot` primary adapter: understands Discord interactions - `discord-bot` app layer: understands command behavior - `discord-bot` secondary adapter: understands how to call `ha-gateway` - `ha-gateway` primary adapter: understands incoming gRPC requests - `ha-gateway` app layer: understands home-control use cases - `ha-gateway` secondary adapter: understands Home Assistant REST This gives you clean boundaries: - change Discord UX without changing Home Assistant integration - change Home Assistant REST details without changing Discord commands - add another client later, like CLI or Alexa, using the same gRPC contract ## `proto/` vs `gen/` in Simple Terms Think of it like this: - `proto/` is the design - `gen/` is the compiled implementation of that design for Go Example: - `proto/ha/v1/light.proto` says there is a `LightService` with `TurnOn` - `gen/ha/v1/light_grpc.pb.go` generates the Go client/server plumbing - `gen/ha/v1/light.pb.go` generates the Go message structs Both services use the same generated package: - same method names - same fields - same wire format That is what keeps both sides compatible. ## Current Service Status ### Implemented in `ha-gateway` - `ha.v1.EntityService` - `GetState` - `ListStates` - `ha.v1.LightService` - `ListLights` - `TurnOn` - `TurnOff` - `Toggle` - `ha.v1.SwitchService` - `ListSwitches` ### Partially Stubbed in `ha-gateway` - `ha.v1.SwitchService` - `TurnOn` - `TurnOff` - `Toggle` ### Stubbed in `ha-gateway` - `ha.v1.EventService` ## Workspace Setup This repo uses a Go workspace: ```text go.work -> ./gen -> ./ha-gateway -> ./discord-bot ``` That lets local modules import each other without publishing intermediate versions while developing. ## Configuration ### `ha-gateway` Sample env file: [ha-gateway/.env.example](https://gitea.nik4nao.com/nik/home-services/src/branch/main/ha-gateway/.env.example) | Variable | Required | Default | Notes | | --- | --- | --- | --- | | `HA_TOKEN` | yes | none | Home Assistant long-lived access token | | `GRPC_PORT` | no | `50051` | gRPC listen port | | `HA_BASE_URL` | effectively yes | empty | Example: `http://ha.home.arpa:8123` | | `OTEL_ENDPOINT` | no | empty | OTLP gRPC endpoint | ### `discord-bot` Sample env file: [discord-bot/.env.example](https://gitea.nik4nao.com/nik/home-services/src/branch/main/discord-bot/.env.example) | Variable | Required | Notes | | --- | --- | --- | | `DISCORD_TOKEN` | yes | Discord bot token | | `GUILD_ID` | no | Guild command registration target | | `HA_GATEWAY_ADDR` | yes | gRPC address of `ha-gateway` | | `OTEL_ENDPOINT` | no | OTLP gRPC endpoint | ## Generate Protobuf Code Run from the repo root: ```bash buf generate ``` That regenerates `gen/ha/v1/*.pb.go` from `proto/ha/v1/*.proto`. ## Build Build each module from its own directory: ```bash cd gen && go build ./... cd ../ha-gateway && go build ./... cd ../discord-bot && go build ./... ``` ## Run Locally ### Start `ha-gateway` ```bash cp ha-gateway/.env.example ha-gateway/.env cd ha-gateway go run ./cmd/gateway ``` ### Start `discord-bot` ```bash cp discord-bot/.env.example discord-bot/.env cd discord-bot go run ./cmd/bot ``` ## Smoke Test `ha-gateway` Because `ha-gateway` registers gRPC reflection, you can inspect it with `grpcurl`: ```bash grpcurl -plaintext localhost:50051 list grpcurl -plaintext localhost:50051 describe ha.v1.LightService ``` Examples: ```bash 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 ha.v1.SwitchService/ListSwitches ``` ## Docker Build from the repo root: ```bash docker build -f ha-gateway/Dockerfile -t ha-gateway:dev . docker build -f discord-bot/Dockerfile -t discord-bot:dev . ``` ## Telemetry Both runtime services support OTLP/gRPC telemetry: - `ha-gateway` uses service name `ha-gateway` - `discord-bot` uses service name `discord-bot` If `OTEL_ENDPOINT` is empty, telemetry is disabled. ## Current Limitations - no auth on inbound gRPC requests yet - `SwitchService` control RPCs are still unimplemented - `EventService` is still unimplemented - Home Assistant event streaming over WebSocket is not implemented - there are currently no unit tests in the repo