12 KiB
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 protobufgen/: generated Go code from that contractha-gateway/: the gRPC server that talks to Home Assistantdiscord-bot/: a Discord client that talks toha-gateway
If you are new to gRPC, the most important idea is:
- define the API once in
.protofiles - generate client/server code from that definition
- let all services use the same generated types so they agree on request and response shape
What Each Folder Does
.
├── 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
LightServiceandSwitchService - RPC methods like
ListLights,TurnOn, andListSwitches - message schemas like
TurnOnRequest,LightEntity, andSwitchEntity
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.golight_grpc.pb.goswitch.pb.goswitch_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-botimportsgitea.nik4nao.com/nik/home-services/gen/ha/v1to get generated gRPC clients likehav1.LightServiceClientha-gatewayimports the same package to register generated gRPC servers likehav1.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:
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
and registers a handler from
handler.go.
When the user runs:
/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, the command is routed by command name and subcommand:
light.ongoes toHandleLightOn(...)
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,
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,
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-botdoes 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,
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.
This is the exact mirror image of the client side:
discord-botuses generated client codeha-gatewayuses generated server code
7. The gRPC adapter maps protobuf types into domain input
In
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:
{
"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,
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:
- Home Assistant returns JSON
ha-gatewaymaps it into internal stateha-gatewaymaps internal state into generated protobuf response types- generated gRPC server code serializes the response
- generated gRPC client code in
discord-botdeserializes it discord-botapp layer formats a human response like:Turned on 'Desk Lamp' (brightness 80%, 3000K).- 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 contractgen/: generated transport code from the contractdiscord-botprimary adapter: understands Discord interactionsdiscord-botapp layer: understands command behaviordiscord-botsecondary adapter: understands how to callha-gatewayha-gatewayprimary adapter: understands incoming gRPC requestsha-gatewayapp layer: understands home-control use casesha-gatewaysecondary 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 designgen/is the compiled implementation of that design for Go
Example:
proto/ha/v1/light.protosays there is aLightServicewithTurnOngen/ha/v1/light_grpc.pb.gogenerates the Go client/server plumbinggen/ha/v1/light.pb.gogenerates 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.EntityServiceGetStateListStates
ha.v1.LightServiceListLightsTurnOnTurnOffToggle
ha.v1.SwitchServiceListSwitches
Partially Stubbed in ha-gateway
ha.v1.SwitchServiceTurnOnTurnOffToggle
Stubbed in ha-gateway
ha.v1.EventService
Workspace Setup
This repo uses a Go workspace:
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
| 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
| 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:
buf generate
That regenerates gen/ha/v1/*.pb.go from proto/ha/v1/*.proto.
Build
Build each module from its own directory:
cd gen && go build ./...
cd ../ha-gateway && go build ./...
cd ../discord-bot && go build ./...
Run Locally
Start ha-gateway
cp ha-gateway/.env.example ha-gateway/.env
cd ha-gateway
go run ./cmd/gateway
Start discord-bot
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:
grpcurl -plaintext localhost:50051 list
grpcurl -plaintext localhost:50051 describe ha.v1.LightService
Examples:
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:
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-gatewayuses service nameha-gatewaydiscord-botuses service namediscord-bot
If OTEL_ENDPOINT is empty, telemetry is disabled.
Current Limitations
- no auth on inbound gRPC requests yet
SwitchServicecontrol RPCs are still unimplementedEventServiceis still unimplemented- Home Assistant event streaming over WebSocket is not implemented
- there are currently no unit tests in the repo