feat: implement SwitchService with ListSwitches method

- Added ListSwitches method to SwitchService in switch_grpc.pb.go.
- Implemented SwitchGRPC adapter for ListSwitches in switch.go.
- Created SwitchApp for managing switch states and added ListSwitches logic.
- Updated core domain with Switch struct and associated methods.
- Enhanced LightApp to include ListLights functionality.
- Updated protobuf definitions for Switch and Light services to include new request and response messages.
- Introduced error handling for unimplemented methods in the gRPC server.
This commit is contained in:
Nik Afiq 2026-04-06 19:25:06 +09:00
parent 2e99c464ff
commit abb6774b77
20 changed files with 1003 additions and 101 deletions

185
README.md
View File

@ -0,0 +1,185 @@
# home-service
This workspace contains a Home Assistant gRPC gateway and the protobuf contract
it serves.
The current implementation is centered on `ha-gateway`, a Go service that:
- exposes Home Assistant operations over gRPC
- translates gRPC requests into Home Assistant REST API calls
- emits OpenTelemetry traces and metrics when configured
- keeps protobuf definitions and generated Go stubs in-repo
`EntityService` and `LightService` are implemented. `SwitchService` and
`EventService` are scaffolded but currently return `Unimplemented`.
## Workspace Layout
```text
.
├── proto/ # Source protobuf definitions
├── gen/ # Generated Go protobuf/grpc code (committed)
├── ha-gateway/ # Go gRPC server
├── buf.yaml # Buf module config
├── buf.gen.yaml # Buf codegen config
└── go.work # Go workspace linking gen + ha-gateway
```
## Architecture
`ha-gateway` follows a ports-and-adapters structure:
- `internal/core/domain`: pure domain types
- `internal/core/ports/driving`: interfaces exposed to primary adapters
- `internal/core/ports/driven`: interfaces the app layer depends on
- `internal/app`: application logic for entity and light operations
- `internal/adapters/primary/grpc`: gRPC handlers and proto/domain mapping
- `internal/adapters/secondary/ha`: Home Assistant REST client
- `internal/telemetry`: OpenTelemetry setup
The runtime flow is:
1. A gRPC client calls `EntityService` or `LightService`.
2. The gRPC adapter maps protobuf messages into domain parameters.
3. The app layer orchestrates the use case.
4. The HA adapter calls Home Assistant's REST API.
5. The response is mapped back into the protobuf response.
## Services
### Implemented
- `ha.v1.EntityService`
- `GetState`
- `ListStates`
- `ha.v1.LightService`
- `TurnOn`
- `TurnOff`
- `Toggle`
### Stubbed
- `ha.v1.SwitchService`
- `ha.v1.EventService`
The event path is planned around Home Assistant WebSocket subscriptions, but the
WebSocket adapter and fan-out broker are not implemented yet.
## Configuration
`ha-gateway` reads configuration from environment variables. A sample file lives
at [ha-gateway/.env.example](/Users/nik-macbookair/repo/home-service/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; empty disables telemetry |
Notes:
- startup fails if `HA_TOKEN` is missing
- `HA_BASE_URL` is not validated on load, but the gateway cannot reach Home
Assistant without it
- if `OTEL_ENDPOINT` is empty, the service installs no-op telemetry providers
## Prerequisites
- Go `1.26`
- `buf` for protobuf generation
- a reachable Home Assistant instance
- a valid Home Assistant long-lived access token
## Generate Protobuf Code
Run from the repo root:
```bash
buf generate
```
Generated files are written to `gen/ha/v1`.
## Build
Sync the workspace, then build the gateway:
```bash
go work sync
cd ha-gateway
go build ./...
```
## Run Locally
Create a local env file:
```bash
cp ha-gateway/.env.example ha-gateway/.env
```
Fill in `HA_TOKEN` and `HA_BASE_URL`, then start the server:
```bash
cd ha-gateway
go run ./cmd/gateway
```
The gateway listens on `:50051` by default.
## Smoke Test With grpcurl
Examples against a locally running gateway:
```bash
# List all light entities
grpcurl -plaintext -d '{"domain":"light"}' \
localhost:50051 ha.v1.EntityService/ListStates
# Get one entity
grpcurl -plaintext -d '{"entity_id":"light.living_room"}' \
localhost:50051 ha.v1.EntityService/GetState
# Turn on a light at 80% brightness
grpcurl -plaintext -d '{"entity_id":"light.living_room","brightness_pct":80}' \
localhost:50051 ha.v1.LightService/TurnOn
# Toggle a light
grpcurl -plaintext -d '{"entity_id":"light.living_room"}' \
localhost:50051 ha.v1.LightService/Toggle
```
## Docker
Build from the repo root:
```bash
docker build -f ha-gateway/Dockerfile -t ha-gateway:dev .
```
Run it with the same env file:
```bash
docker run --env-file ha-gateway/.env -p 50051:50051 ha-gateway:dev
```
## Telemetry
When `OTEL_ENDPOINT` is set, the gateway exports:
- traces via OTLP/gRPC
- metrics via OTLP/gRPC
The service name is `ha-gateway`. When `OTEL_ENDPOINT` is unset, telemetry is
disabled for local development.
## Current Limitations
- no authentication/authorization on inbound gRPC requests yet
- `SwitchService` is not implemented
- `EventService` is not implemented
- Home Assistant event streaming over WebSocket is not implemented
- there are currently no unit tests in the repo
The auth note in [ha-gateway/cmd/gateway/main.go](/Users/nik-macbookair/repo/home-service/ha-gateway/cmd/gateway/main.go) explicitly calls out API-key and mTLS as future options before exposing the gateway outside a trusted network.

View File

@ -240,6 +240,186 @@ func (x *LightResponse) GetState() *EntityState {
return nil return nil
} }
type LightEntity struct {
state protoimpl.MessageState `protogen:"open.v1"`
EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"`
FriendlyName string `protobuf:"bytes,2,opt,name=friendly_name,json=friendlyName,proto3" json:"friendly_name,omitempty"`
State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"`
SupportedColorModes []string `protobuf:"bytes,4,rep,name=supported_color_modes,json=supportedColorModes,proto3" json:"supported_color_modes,omitempty"`
MinColorTempKelvin uint32 `protobuf:"varint,5,opt,name=min_color_temp_kelvin,json=minColorTempKelvin,proto3" json:"min_color_temp_kelvin,omitempty"`
MaxColorTempKelvin uint32 `protobuf:"varint,6,opt,name=max_color_temp_kelvin,json=maxColorTempKelvin,proto3" json:"max_color_temp_kelvin,omitempty"`
IsHueGroup bool `protobuf:"varint,7,opt,name=is_hue_group,json=isHueGroup,proto3" json:"is_hue_group,omitempty"`
EffectList []string `protobuf:"bytes,8,rep,name=effect_list,json=effectList,proto3" json:"effect_list,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *LightEntity) Reset() {
*x = LightEntity{}
mi := &file_ha_v1_light_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *LightEntity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*LightEntity) ProtoMessage() {}
func (x *LightEntity) ProtoReflect() protoreflect.Message {
mi := &file_ha_v1_light_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use LightEntity.ProtoReflect.Descriptor instead.
func (*LightEntity) Descriptor() ([]byte, []int) {
return file_ha_v1_light_proto_rawDescGZIP(), []int{4}
}
func (x *LightEntity) GetEntityId() string {
if x != nil {
return x.EntityId
}
return ""
}
func (x *LightEntity) GetFriendlyName() string {
if x != nil {
return x.FriendlyName
}
return ""
}
func (x *LightEntity) GetState() string {
if x != nil {
return x.State
}
return ""
}
func (x *LightEntity) GetSupportedColorModes() []string {
if x != nil {
return x.SupportedColorModes
}
return nil
}
func (x *LightEntity) GetMinColorTempKelvin() uint32 {
if x != nil {
return x.MinColorTempKelvin
}
return 0
}
func (x *LightEntity) GetMaxColorTempKelvin() uint32 {
if x != nil {
return x.MaxColorTempKelvin
}
return 0
}
func (x *LightEntity) GetIsHueGroup() bool {
if x != nil {
return x.IsHueGroup
}
return false
}
func (x *LightEntity) GetEffectList() []string {
if x != nil {
return x.EffectList
}
return nil
}
type ListLightsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListLightsRequest) Reset() {
*x = ListLightsRequest{}
mi := &file_ha_v1_light_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListLightsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListLightsRequest) ProtoMessage() {}
func (x *ListLightsRequest) ProtoReflect() protoreflect.Message {
mi := &file_ha_v1_light_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListLightsRequest.ProtoReflect.Descriptor instead.
func (*ListLightsRequest) Descriptor() ([]byte, []int) {
return file_ha_v1_light_proto_rawDescGZIP(), []int{5}
}
type ListLightsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Lights []*LightEntity `protobuf:"bytes,1,rep,name=lights,proto3" json:"lights,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListLightsResponse) Reset() {
*x = ListLightsResponse{}
mi := &file_ha_v1_light_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListLightsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListLightsResponse) ProtoMessage() {}
func (x *ListLightsResponse) ProtoReflect() protoreflect.Message {
mi := &file_ha_v1_light_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListLightsResponse.ProtoReflect.Descriptor instead.
func (*ListLightsResponse) Descriptor() ([]byte, []int) {
return file_ha_v1_light_proto_rawDescGZIP(), []int{6}
}
func (x *ListLightsResponse) GetLights() []*LightEntity {
if x != nil {
return x.Lights
}
return nil
}
var File_ha_v1_light_proto protoreflect.FileDescriptor var File_ha_v1_light_proto protoreflect.FileDescriptor
const file_ha_v1_light_proto_rawDesc = "" + const file_ha_v1_light_proto_rawDesc = "" +
@ -267,11 +447,27 @@ const file_ha_v1_light_proto_rawDesc = "" +
"\rToggleRequest\x12\x1b\n" + "\rToggleRequest\x12\x1b\n" +
"\tentity_id\x18\x01 \x01(\tR\bentityId\"9\n" + "\tentity_id\x18\x01 \x01(\tR\bentityId\"9\n" +
"\rLightResponse\x12(\n" + "\rLightResponse\x12(\n" +
"\x05state\x18\x01 \x01(\v2\x12.ha.v1.EntityStateR\x05state2\xb2\x01\n" + "\x05state\x18\x01 \x01(\v2\x12.ha.v1.EntityStateR\x05state\"\xc2\x02\n" +
"\vLightEntity\x12\x1b\n" +
"\tentity_id\x18\x01 \x01(\tR\bentityId\x12#\n" +
"\rfriendly_name\x18\x02 \x01(\tR\ffriendlyName\x12\x14\n" +
"\x05state\x18\x03 \x01(\tR\x05state\x122\n" +
"\x15supported_color_modes\x18\x04 \x03(\tR\x13supportedColorModes\x121\n" +
"\x15min_color_temp_kelvin\x18\x05 \x01(\rR\x12minColorTempKelvin\x121\n" +
"\x15max_color_temp_kelvin\x18\x06 \x01(\rR\x12maxColorTempKelvin\x12 \n" +
"\fis_hue_group\x18\a \x01(\bR\n" +
"isHueGroup\x12\x1f\n" +
"\veffect_list\x18\b \x03(\tR\n" +
"effectList\"\x13\n" +
"\x11ListLightsRequest\"@\n" +
"\x12ListLightsResponse\x12*\n" +
"\x06lights\x18\x01 \x03(\v2\x12.ha.v1.LightEntityR\x06lights2\xf5\x01\n" +
"\fLightService\x124\n" + "\fLightService\x124\n" +
"\x06TurnOn\x12\x14.ha.v1.TurnOnRequest\x1a\x14.ha.v1.LightResponse\x126\n" + "\x06TurnOn\x12\x14.ha.v1.TurnOnRequest\x1a\x14.ha.v1.LightResponse\x126\n" +
"\aTurnOff\x12\x15.ha.v1.TurnOffRequest\x1a\x14.ha.v1.LightResponse\x124\n" + "\aTurnOff\x12\x15.ha.v1.TurnOffRequest\x1a\x14.ha.v1.LightResponse\x124\n" +
"\x06Toggle\x12\x14.ha.v1.ToggleRequest\x1a\x14.ha.v1.LightResponseB4Z2gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1b\x06proto3" "\x06Toggle\x12\x14.ha.v1.ToggleRequest\x1a\x14.ha.v1.LightResponse\x12A\n" +
"\n" +
"ListLights\x12\x18.ha.v1.ListLightsRequest\x1a\x19.ha.v1.ListLightsResponseB4Z2gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1b\x06proto3"
var ( var (
file_ha_v1_light_proto_rawDescOnce sync.Once file_ha_v1_light_proto_rawDescOnce sync.Once
@ -285,29 +481,35 @@ func file_ha_v1_light_proto_rawDescGZIP() []byte {
return file_ha_v1_light_proto_rawDescData return file_ha_v1_light_proto_rawDescData
} }
var file_ha_v1_light_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_ha_v1_light_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
var file_ha_v1_light_proto_goTypes = []any{ var file_ha_v1_light_proto_goTypes = []any{
(*TurnOnRequest)(nil), // 0: ha.v1.TurnOnRequest (*TurnOnRequest)(nil), // 0: ha.v1.TurnOnRequest
(*TurnOffRequest)(nil), // 1: ha.v1.TurnOffRequest (*TurnOffRequest)(nil), // 1: ha.v1.TurnOffRequest
(*ToggleRequest)(nil), // 2: ha.v1.ToggleRequest (*ToggleRequest)(nil), // 2: ha.v1.ToggleRequest
(*LightResponse)(nil), // 3: ha.v1.LightResponse (*LightResponse)(nil), // 3: ha.v1.LightResponse
(*RGBColor)(nil), // 4: ha.v1.RGBColor (*LightEntity)(nil), // 4: ha.v1.LightEntity
(*EntityState)(nil), // 5: ha.v1.EntityState (*ListLightsRequest)(nil), // 5: ha.v1.ListLightsRequest
(*ListLightsResponse)(nil), // 6: ha.v1.ListLightsResponse
(*RGBColor)(nil), // 7: ha.v1.RGBColor
(*EntityState)(nil), // 8: ha.v1.EntityState
} }
var file_ha_v1_light_proto_depIdxs = []int32{ var file_ha_v1_light_proto_depIdxs = []int32{
4, // 0: ha.v1.TurnOnRequest.rgb_color:type_name -> ha.v1.RGBColor 7, // 0: ha.v1.TurnOnRequest.rgb_color:type_name -> ha.v1.RGBColor
5, // 1: ha.v1.LightResponse.state:type_name -> ha.v1.EntityState 8, // 1: ha.v1.LightResponse.state:type_name -> ha.v1.EntityState
0, // 2: ha.v1.LightService.TurnOn:input_type -> ha.v1.TurnOnRequest 4, // 2: ha.v1.ListLightsResponse.lights:type_name -> ha.v1.LightEntity
1, // 3: ha.v1.LightService.TurnOff:input_type -> ha.v1.TurnOffRequest 0, // 3: ha.v1.LightService.TurnOn:input_type -> ha.v1.TurnOnRequest
2, // 4: ha.v1.LightService.Toggle:input_type -> ha.v1.ToggleRequest 1, // 4: ha.v1.LightService.TurnOff:input_type -> ha.v1.TurnOffRequest
3, // 5: ha.v1.LightService.TurnOn:output_type -> ha.v1.LightResponse 2, // 5: ha.v1.LightService.Toggle:input_type -> ha.v1.ToggleRequest
3, // 6: ha.v1.LightService.TurnOff:output_type -> ha.v1.LightResponse 5, // 6: ha.v1.LightService.ListLights:input_type -> ha.v1.ListLightsRequest
3, // 7: ha.v1.LightService.Toggle:output_type -> ha.v1.LightResponse 3, // 7: ha.v1.LightService.TurnOn:output_type -> ha.v1.LightResponse
5, // [5:8] is the sub-list for method output_type 3, // 8: ha.v1.LightService.TurnOff:output_type -> ha.v1.LightResponse
2, // [2:5] is the sub-list for method input_type 3, // 9: ha.v1.LightService.Toggle:output_type -> ha.v1.LightResponse
2, // [2:2] is the sub-list for extension type_name 6, // 10: ha.v1.LightService.ListLights:output_type -> ha.v1.ListLightsResponse
2, // [2:2] is the sub-list for extension extendee 7, // [7:11] is the sub-list for method output_type
0, // [0:2] is the sub-list for field type_name 3, // [3:7] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
} }
func init() { file_ha_v1_light_proto_init() } func init() { file_ha_v1_light_proto_init() }
@ -324,7 +526,7 @@ func file_ha_v1_light_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_light_proto_rawDesc), len(file_ha_v1_light_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_light_proto_rawDesc), len(file_ha_v1_light_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 4, NumMessages: 7,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@ -22,6 +22,7 @@ const (
LightService_TurnOn_FullMethodName = "/ha.v1.LightService/TurnOn" LightService_TurnOn_FullMethodName = "/ha.v1.LightService/TurnOn"
LightService_TurnOff_FullMethodName = "/ha.v1.LightService/TurnOff" LightService_TurnOff_FullMethodName = "/ha.v1.LightService/TurnOff"
LightService_Toggle_FullMethodName = "/ha.v1.LightService/Toggle" LightService_Toggle_FullMethodName = "/ha.v1.LightService/Toggle"
LightService_ListLights_FullMethodName = "/ha.v1.LightService/ListLights"
) )
// LightServiceClient is the client API for LightService service. // LightServiceClient is the client API for LightService service.
@ -31,6 +32,7 @@ type LightServiceClient interface {
TurnOn(ctx context.Context, in *TurnOnRequest, opts ...grpc.CallOption) (*LightResponse, error) TurnOn(ctx context.Context, in *TurnOnRequest, opts ...grpc.CallOption) (*LightResponse, error)
TurnOff(ctx context.Context, in *TurnOffRequest, opts ...grpc.CallOption) (*LightResponse, error) TurnOff(ctx context.Context, in *TurnOffRequest, opts ...grpc.CallOption) (*LightResponse, error)
Toggle(ctx context.Context, in *ToggleRequest, opts ...grpc.CallOption) (*LightResponse, error) Toggle(ctx context.Context, in *ToggleRequest, opts ...grpc.CallOption) (*LightResponse, error)
ListLights(ctx context.Context, in *ListLightsRequest, opts ...grpc.CallOption) (*ListLightsResponse, error)
} }
type lightServiceClient struct { type lightServiceClient struct {
@ -71,6 +73,16 @@ func (c *lightServiceClient) Toggle(ctx context.Context, in *ToggleRequest, opts
return out, nil return out, nil
} }
func (c *lightServiceClient) ListLights(ctx context.Context, in *ListLightsRequest, opts ...grpc.CallOption) (*ListLightsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListLightsResponse)
err := c.cc.Invoke(ctx, LightService_ListLights_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// LightServiceServer is the server API for LightService service. // LightServiceServer is the server API for LightService service.
// All implementations must embed UnimplementedLightServiceServer // All implementations must embed UnimplementedLightServiceServer
// for forward compatibility. // for forward compatibility.
@ -78,6 +90,7 @@ type LightServiceServer interface {
TurnOn(context.Context, *TurnOnRequest) (*LightResponse, error) TurnOn(context.Context, *TurnOnRequest) (*LightResponse, error)
TurnOff(context.Context, *TurnOffRequest) (*LightResponse, error) TurnOff(context.Context, *TurnOffRequest) (*LightResponse, error)
Toggle(context.Context, *ToggleRequest) (*LightResponse, error) Toggle(context.Context, *ToggleRequest) (*LightResponse, error)
ListLights(context.Context, *ListLightsRequest) (*ListLightsResponse, error)
mustEmbedUnimplementedLightServiceServer() mustEmbedUnimplementedLightServiceServer()
} }
@ -97,6 +110,9 @@ func (UnimplementedLightServiceServer) TurnOff(context.Context, *TurnOffRequest)
func (UnimplementedLightServiceServer) Toggle(context.Context, *ToggleRequest) (*LightResponse, error) { func (UnimplementedLightServiceServer) Toggle(context.Context, *ToggleRequest) (*LightResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Toggle not implemented") return nil, status.Error(codes.Unimplemented, "method Toggle not implemented")
} }
func (UnimplementedLightServiceServer) ListLights(context.Context, *ListLightsRequest) (*ListLightsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListLights not implemented")
}
func (UnimplementedLightServiceServer) mustEmbedUnimplementedLightServiceServer() {} func (UnimplementedLightServiceServer) mustEmbedUnimplementedLightServiceServer() {}
func (UnimplementedLightServiceServer) testEmbeddedByValue() {} func (UnimplementedLightServiceServer) testEmbeddedByValue() {}
@ -172,6 +188,24 @@ func _LightService_Toggle_Handler(srv interface{}, ctx context.Context, dec func
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _LightService_ListLights_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListLightsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(LightServiceServer).ListLights(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: LightService_ListLights_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(LightServiceServer).ListLights(ctx, req.(*ListLightsRequest))
}
return interceptor(ctx, in, info, handler)
}
// LightService_ServiceDesc is the grpc.ServiceDesc for LightService service. // LightService_ServiceDesc is the grpc.ServiceDesc for LightService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@ -191,6 +225,10 @@ var LightService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Toggle", MethodName: "Toggle",
Handler: _LightService_Toggle_Handler, Handler: _LightService_Toggle_Handler,
}, },
{
MethodName: "ListLights",
Handler: _LightService_ListLights_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "ha/v1/light.proto", Metadata: "ha/v1/light.proto",

View File

@ -109,6 +109,154 @@ func (x *SwitchResponse) GetState() *EntityState {
return nil return nil
} }
type SwitchEntity struct {
state protoimpl.MessageState `protogen:"open.v1"`
EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"`
FriendlyName string `protobuf:"bytes,2,opt,name=friendly_name,json=friendlyName,proto3" json:"friendly_name,omitempty"`
State string `protobuf:"bytes,3,opt,name=state,proto3" json:"state,omitempty"`
DeviceClass string `protobuf:"bytes,4,opt,name=device_class,json=deviceClass,proto3" json:"device_class,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SwitchEntity) Reset() {
*x = SwitchEntity{}
mi := &file_ha_v1_switch_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SwitchEntity) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SwitchEntity) ProtoMessage() {}
func (x *SwitchEntity) ProtoReflect() protoreflect.Message {
mi := &file_ha_v1_switch_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SwitchEntity.ProtoReflect.Descriptor instead.
func (*SwitchEntity) Descriptor() ([]byte, []int) {
return file_ha_v1_switch_proto_rawDescGZIP(), []int{2}
}
func (x *SwitchEntity) GetEntityId() string {
if x != nil {
return x.EntityId
}
return ""
}
func (x *SwitchEntity) GetFriendlyName() string {
if x != nil {
return x.FriendlyName
}
return ""
}
func (x *SwitchEntity) GetState() string {
if x != nil {
return x.State
}
return ""
}
func (x *SwitchEntity) GetDeviceClass() string {
if x != nil {
return x.DeviceClass
}
return ""
}
type ListSwitchesRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListSwitchesRequest) Reset() {
*x = ListSwitchesRequest{}
mi := &file_ha_v1_switch_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListSwitchesRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListSwitchesRequest) ProtoMessage() {}
func (x *ListSwitchesRequest) ProtoReflect() protoreflect.Message {
mi := &file_ha_v1_switch_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListSwitchesRequest.ProtoReflect.Descriptor instead.
func (*ListSwitchesRequest) Descriptor() ([]byte, []int) {
return file_ha_v1_switch_proto_rawDescGZIP(), []int{3}
}
type ListSwitchesResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Switches []*SwitchEntity `protobuf:"bytes,1,rep,name=switches,proto3" json:"switches,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListSwitchesResponse) Reset() {
*x = ListSwitchesResponse{}
mi := &file_ha_v1_switch_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListSwitchesResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListSwitchesResponse) ProtoMessage() {}
func (x *ListSwitchesResponse) ProtoReflect() protoreflect.Message {
mi := &file_ha_v1_switch_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListSwitchesResponse.ProtoReflect.Descriptor instead.
func (*ListSwitchesResponse) Descriptor() ([]byte, []int) {
return file_ha_v1_switch_proto_rawDescGZIP(), []int{4}
}
func (x *ListSwitchesResponse) GetSwitches() []*SwitchEntity {
if x != nil {
return x.Switches
}
return nil
}
var File_ha_v1_switch_proto protoreflect.FileDescriptor var File_ha_v1_switch_proto protoreflect.FileDescriptor
const file_ha_v1_switch_proto_rawDesc = "" + const file_ha_v1_switch_proto_rawDesc = "" +
@ -117,11 +265,20 @@ const file_ha_v1_switch_proto_rawDesc = "" +
"\rSwitchRequest\x12\x1b\n" + "\rSwitchRequest\x12\x1b\n" +
"\tentity_id\x18\x01 \x01(\tR\bentityId\":\n" + "\tentity_id\x18\x01 \x01(\tR\bentityId\":\n" +
"\x0eSwitchResponse\x12(\n" + "\x0eSwitchResponse\x12(\n" +
"\x05state\x18\x01 \x01(\v2\x12.ha.v1.EntityStateR\x05state2\xb5\x01\n" + "\x05state\x18\x01 \x01(\v2\x12.ha.v1.EntityStateR\x05state\"\x89\x01\n" +
"\fSwitchEntity\x12\x1b\n" +
"\tentity_id\x18\x01 \x01(\tR\bentityId\x12#\n" +
"\rfriendly_name\x18\x02 \x01(\tR\ffriendlyName\x12\x14\n" +
"\x05state\x18\x03 \x01(\tR\x05state\x12!\n" +
"\fdevice_class\x18\x04 \x01(\tR\vdeviceClass\"\x15\n" +
"\x13ListSwitchesRequest\"G\n" +
"\x14ListSwitchesResponse\x12/\n" +
"\bswitches\x18\x01 \x03(\v2\x13.ha.v1.SwitchEntityR\bswitches2\xfe\x01\n" +
"\rSwitchService\x125\n" + "\rSwitchService\x125\n" +
"\x06TurnOn\x12\x14.ha.v1.SwitchRequest\x1a\x15.ha.v1.SwitchResponse\x126\n" + "\x06TurnOn\x12\x14.ha.v1.SwitchRequest\x1a\x15.ha.v1.SwitchResponse\x126\n" +
"\aTurnOff\x12\x14.ha.v1.SwitchRequest\x1a\x15.ha.v1.SwitchResponse\x125\n" + "\aTurnOff\x12\x14.ha.v1.SwitchRequest\x1a\x15.ha.v1.SwitchResponse\x125\n" +
"\x06Toggle\x12\x14.ha.v1.SwitchRequest\x1a\x15.ha.v1.SwitchResponseB4Z2gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1b\x06proto3" "\x06Toggle\x12\x14.ha.v1.SwitchRequest\x1a\x15.ha.v1.SwitchResponse\x12G\n" +
"\fListSwitches\x12\x1a.ha.v1.ListSwitchesRequest\x1a\x1b.ha.v1.ListSwitchesResponseB4Z2gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1b\x06proto3"
var ( var (
file_ha_v1_switch_proto_rawDescOnce sync.Once file_ha_v1_switch_proto_rawDescOnce sync.Once
@ -135,25 +292,31 @@ func file_ha_v1_switch_proto_rawDescGZIP() []byte {
return file_ha_v1_switch_proto_rawDescData return file_ha_v1_switch_proto_rawDescData
} }
var file_ha_v1_switch_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_ha_v1_switch_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_ha_v1_switch_proto_goTypes = []any{ var file_ha_v1_switch_proto_goTypes = []any{
(*SwitchRequest)(nil), // 0: ha.v1.SwitchRequest (*SwitchRequest)(nil), // 0: ha.v1.SwitchRequest
(*SwitchResponse)(nil), // 1: ha.v1.SwitchResponse (*SwitchResponse)(nil), // 1: ha.v1.SwitchResponse
(*EntityState)(nil), // 2: ha.v1.EntityState (*SwitchEntity)(nil), // 2: ha.v1.SwitchEntity
(*ListSwitchesRequest)(nil), // 3: ha.v1.ListSwitchesRequest
(*ListSwitchesResponse)(nil), // 4: ha.v1.ListSwitchesResponse
(*EntityState)(nil), // 5: ha.v1.EntityState
} }
var file_ha_v1_switch_proto_depIdxs = []int32{ var file_ha_v1_switch_proto_depIdxs = []int32{
2, // 0: ha.v1.SwitchResponse.state:type_name -> ha.v1.EntityState 5, // 0: ha.v1.SwitchResponse.state:type_name -> ha.v1.EntityState
0, // 1: ha.v1.SwitchService.TurnOn:input_type -> ha.v1.SwitchRequest 2, // 1: ha.v1.ListSwitchesResponse.switches:type_name -> ha.v1.SwitchEntity
0, // 2: ha.v1.SwitchService.TurnOff:input_type -> ha.v1.SwitchRequest 0, // 2: ha.v1.SwitchService.TurnOn:input_type -> ha.v1.SwitchRequest
0, // 3: ha.v1.SwitchService.Toggle:input_type -> ha.v1.SwitchRequest 0, // 3: ha.v1.SwitchService.TurnOff:input_type -> ha.v1.SwitchRequest
1, // 4: ha.v1.SwitchService.TurnOn:output_type -> ha.v1.SwitchResponse 0, // 4: ha.v1.SwitchService.Toggle:input_type -> ha.v1.SwitchRequest
1, // 5: ha.v1.SwitchService.TurnOff:output_type -> ha.v1.SwitchResponse 3, // 5: ha.v1.SwitchService.ListSwitches:input_type -> ha.v1.ListSwitchesRequest
1, // 6: ha.v1.SwitchService.Toggle:output_type -> ha.v1.SwitchResponse 1, // 6: ha.v1.SwitchService.TurnOn:output_type -> ha.v1.SwitchResponse
4, // [4:7] is the sub-list for method output_type 1, // 7: ha.v1.SwitchService.TurnOff:output_type -> ha.v1.SwitchResponse
1, // [1:4] is the sub-list for method input_type 1, // 8: ha.v1.SwitchService.Toggle:output_type -> ha.v1.SwitchResponse
1, // [1:1] is the sub-list for extension type_name 4, // 9: ha.v1.SwitchService.ListSwitches:output_type -> ha.v1.ListSwitchesResponse
1, // [1:1] is the sub-list for extension extendee 6, // [6:10] is the sub-list for method output_type
0, // [0:1] is the sub-list for field type_name 2, // [2:6] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
} }
func init() { file_ha_v1_switch_proto_init() } func init() { file_ha_v1_switch_proto_init() }
@ -168,7 +331,7 @@ func file_ha_v1_switch_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_switch_proto_rawDesc), len(file_ha_v1_switch_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_switch_proto_rawDesc), len(file_ha_v1_switch_proto_rawDesc)),
NumEnums: 0, NumEnums: 0,
NumMessages: 2, NumMessages: 5,
NumExtensions: 0, NumExtensions: 0,
NumServices: 1, NumServices: 1,
}, },

View File

@ -22,21 +22,17 @@ const (
SwitchService_TurnOn_FullMethodName = "/ha.v1.SwitchService/TurnOn" SwitchService_TurnOn_FullMethodName = "/ha.v1.SwitchService/TurnOn"
SwitchService_TurnOff_FullMethodName = "/ha.v1.SwitchService/TurnOff" SwitchService_TurnOff_FullMethodName = "/ha.v1.SwitchService/TurnOff"
SwitchService_Toggle_FullMethodName = "/ha.v1.SwitchService/Toggle" SwitchService_Toggle_FullMethodName = "/ha.v1.SwitchService/Toggle"
SwitchService_ListSwitches_FullMethodName = "/ha.v1.SwitchService/ListSwitches"
) )
// SwitchServiceClient is the client API for SwitchService service. // SwitchServiceClient is the client API for SwitchService service.
// //
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// TODO: implement SwitchService. Follow the same pattern as LightService:
// - domain type in core/domain/switch.go
// - port interface in core/ports/driving/switch.go
// - application logic in app/switch.go
// - gRPC adapter in adapters/primary/grpc/switch.go
type SwitchServiceClient interface { type SwitchServiceClient interface {
TurnOn(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error) TurnOn(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error)
TurnOff(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error) TurnOff(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error)
Toggle(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error) Toggle(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error)
ListSwitches(ctx context.Context, in *ListSwitchesRequest, opts ...grpc.CallOption) (*ListSwitchesResponse, error)
} }
type switchServiceClient struct { type switchServiceClient struct {
@ -77,19 +73,24 @@ func (c *switchServiceClient) Toggle(ctx context.Context, in *SwitchRequest, opt
return out, nil return out, nil
} }
func (c *switchServiceClient) ListSwitches(ctx context.Context, in *ListSwitchesRequest, opts ...grpc.CallOption) (*ListSwitchesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListSwitchesResponse)
err := c.cc.Invoke(ctx, SwitchService_ListSwitches_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// SwitchServiceServer is the server API for SwitchService service. // SwitchServiceServer is the server API for SwitchService service.
// All implementations must embed UnimplementedSwitchServiceServer // All implementations must embed UnimplementedSwitchServiceServer
// for forward compatibility. // for forward compatibility.
//
// TODO: implement SwitchService. Follow the same pattern as LightService:
// - domain type in core/domain/switch.go
// - port interface in core/ports/driving/switch.go
// - application logic in app/switch.go
// - gRPC adapter in adapters/primary/grpc/switch.go
type SwitchServiceServer interface { type SwitchServiceServer interface {
TurnOn(context.Context, *SwitchRequest) (*SwitchResponse, error) TurnOn(context.Context, *SwitchRequest) (*SwitchResponse, error)
TurnOff(context.Context, *SwitchRequest) (*SwitchResponse, error) TurnOff(context.Context, *SwitchRequest) (*SwitchResponse, error)
Toggle(context.Context, *SwitchRequest) (*SwitchResponse, error) Toggle(context.Context, *SwitchRequest) (*SwitchResponse, error)
ListSwitches(context.Context, *ListSwitchesRequest) (*ListSwitchesResponse, error)
mustEmbedUnimplementedSwitchServiceServer() mustEmbedUnimplementedSwitchServiceServer()
} }
@ -109,6 +110,9 @@ func (UnimplementedSwitchServiceServer) TurnOff(context.Context, *SwitchRequest)
func (UnimplementedSwitchServiceServer) Toggle(context.Context, *SwitchRequest) (*SwitchResponse, error) { func (UnimplementedSwitchServiceServer) Toggle(context.Context, *SwitchRequest) (*SwitchResponse, error) {
return nil, status.Error(codes.Unimplemented, "method Toggle not implemented") return nil, status.Error(codes.Unimplemented, "method Toggle not implemented")
} }
func (UnimplementedSwitchServiceServer) ListSwitches(context.Context, *ListSwitchesRequest) (*ListSwitchesResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListSwitches not implemented")
}
func (UnimplementedSwitchServiceServer) mustEmbedUnimplementedSwitchServiceServer() {} func (UnimplementedSwitchServiceServer) mustEmbedUnimplementedSwitchServiceServer() {}
func (UnimplementedSwitchServiceServer) testEmbeddedByValue() {} func (UnimplementedSwitchServiceServer) testEmbeddedByValue() {}
@ -184,6 +188,24 @@ func _SwitchService_Toggle_Handler(srv interface{}, ctx context.Context, dec fun
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _SwitchService_ListSwitches_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListSwitchesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SwitchServiceServer).ListSwitches(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SwitchService_ListSwitches_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SwitchServiceServer).ListSwitches(ctx, req.(*ListSwitchesRequest))
}
return interceptor(ctx, in, info, handler)
}
// SwitchService_ServiceDesc is the grpc.ServiceDesc for SwitchService service. // SwitchService_ServiceDesc is the grpc.ServiceDesc for SwitchService service.
// It's only intended for direct use with grpc.RegisterService, // It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy) // and not to be introspected or modified (even as a copy)
@ -203,6 +225,10 @@ var SwitchService_ServiceDesc = grpc.ServiceDesc{
MethodName: "Toggle", MethodName: "Toggle",
Handler: _SwitchService_Toggle_Handler, Handler: _SwitchService_Toggle_Handler,
}, },
{
MethodName: "ListSwitches",
Handler: _SwitchService_ListSwitches_Handler,
},
}, },
Streams: []grpc.StreamDesc{}, Streams: []grpc.StreamDesc{},
Metadata: "ha/v1/switch.proto", Metadata: "ha/v1/switch.proto",

View File

@ -4,12 +4,10 @@ cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1F
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0/go.mod h1:obipzmGjfSjam60XLwGfqUkJsfiheAl+TUjG+4yzyPM=
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/cncf/xds/go v0.0.0-20241223141626-cff3c89139a3/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
@ -19,38 +17,26 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.4/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo= go.opentelemetry.io/contrib/detectors/gcp v1.34.0/go.mod h1:cV4BMFcscUR/ckqLkbfQmF0PRsq8w/lMGzdbCSveBHo=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk= go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 h1:QcFwRrZLc82r8wODjvyCbP7Ifp3UANaBSmhDSFjnqSc=
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0/go.mod h1:CXIWhUomyWBG/oY2/r/kLp6K/cmx9e/7DLpBuuGdLCA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
@ -59,11 +45,10 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4= go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE= go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
@ -72,13 +57,13 @@ golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
@ -89,4 +74,4 @@ google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwl
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -10,13 +10,13 @@ import (
"time" "time"
"github.com/joho/godotenv" "github.com/joho/godotenv"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/peer" "google.golang.org/grpc/peer"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/adapters/secondary/ha"
grpcadapter "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/adapters/primary/grpc" grpcadapter "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/adapters/primary/grpc"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/adapters/secondary/ha"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/app" "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/app"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/config" "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/config"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/telemetry" "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/telemetry"
@ -61,6 +61,16 @@ func main() {
// 5. Build application services. // 5. Build application services.
entityApp := app.NewEntityApp(haClient) entityApp := app.NewEntityApp(haClient)
lightApp := app.NewLightApp(haClient) lightApp := app.NewLightApp(haClient)
switchApp := app.NewSwitchApp(haClient)
// 5a. Prime the discovery cache. Non-fatal — ListLights/ListSwitches will
// retry lazily on the first call if this fails (e.g. HA not yet ready).
if err := lightApp.Refresh(ctx); err != nil {
slog.Warn("initial light discovery failed, will retry on first request", "err", err)
}
if err := switchApp.Refresh(ctx); err != nil {
slog.Warn("initial switch discovery failed, will retry on first request", "err", err)
}
// 6. Build the gRPC server with OTel stats handler and logging interceptors. // 6. Build the gRPC server with OTel stats handler and logging interceptors.
srv := grpc.NewServer( srv := grpc.NewServer(
@ -72,7 +82,7 @@ func main() {
// 7. Register services. // 7. Register services.
hav1.RegisterEntityServiceServer(srv, grpcadapter.NewEntityGRPC(entityApp)) hav1.RegisterEntityServiceServer(srv, grpcadapter.NewEntityGRPC(entityApp))
hav1.RegisterLightServiceServer(srv, grpcadapter.NewLightGRPC(lightApp)) hav1.RegisterLightServiceServer(srv, grpcadapter.NewLightGRPC(lightApp))
hav1.RegisterSwitchServiceServer(srv, &grpcadapter.SwitchGRPC{}) hav1.RegisterSwitchServiceServer(srv, grpcadapter.NewSwitchGRPC(switchApp))
hav1.RegisterEventServiceServer(srv, &grpcadapter.EventGRPC{}) hav1.RegisterEventServiceServer(srv, &grpcadapter.EventGRPC{})
// 8. Start listener. // 8. Start listener.

View File

@ -52,8 +52,14 @@ func grpcError(err error) error {
if errors.Is(err, ErrNotFound) { if errors.Is(err, ErrNotFound) {
return status.Errorf(codes.NotFound, "%v", err) return status.Errorf(codes.NotFound, "%v", err)
} }
if errors.Is(err, ErrNotImplemented) {
return status.Errorf(codes.Unimplemented, "%v", err)
}
return status.Errorf(codes.Internal, "%v", err) return status.Errorf(codes.Internal, "%v", err)
} }
// ErrNotFound is returned by the app layer when an entity does not exist. // ErrNotFound is returned by the app layer when an entity does not exist.
var ErrNotFound = errors.New("not found") var ErrNotFound = errors.New("not found")
// ErrNotImplemented is returned by handlers that are stubbed pending full implementation.
var ErrNotImplemented = errors.New("not implemented")

View File

@ -40,3 +40,15 @@ func (h *LightGRPC) Toggle(ctx context.Context, req *hav1.ToggleRequest) (*hav1.
} }
return &hav1.LightResponse{State: domainStateToProto(s)}, nil return &hav1.LightResponse{State: domainStateToProto(s)}, nil
} }
func (h *LightGRPC) ListLights(ctx context.Context, req *hav1.ListLightsRequest) (*hav1.ListLightsResponse, error) {
lights, err := h.svc.ListLights(ctx)
if err != nil {
return nil, grpcError(err)
}
out := make([]*hav1.LightEntity, 0, len(lights))
for _, l := range lights {
out = append(out, domainLightToProto(l))
}
return &hav1.ListLightsResponse{Lights: out}, nil
}

View File

@ -1,8 +1,8 @@
package grpc package grpc
import ( import (
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain"
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain"
) )
func domainStateToProto(s *domain.EntityState) *hav1.EntityState { func domainStateToProto(s *domain.EntityState) *hav1.EntityState {
@ -51,3 +51,29 @@ func protoTurnOffToParams(r *hav1.TurnOffRequest) domain.TurnOffParams {
} }
return p return p
} }
func domainLightToProto(l domain.Light) *hav1.LightEntity {
modes := make([]string, len(l.SupportedColorModes))
for i, m := range l.SupportedColorModes {
modes[i] = string(m)
}
return &hav1.LightEntity{
EntityId: string(l.EntityID),
FriendlyName: l.FriendlyName,
State: l.State,
SupportedColorModes: modes,
MinColorTempKelvin: l.MinColorTempKelvin,
MaxColorTempKelvin: l.MaxColorTempKelvin,
IsHueGroup: l.IsHueGroup,
EffectList: l.EffectList,
}
}
func domainSwitchToProto(s domain.Switch) *hav1.SwitchEntity {
return &hav1.SwitchEntity{
EntityId: string(s.EntityID),
FriendlyName: s.FriendlyName,
State: s.State,
DeviceClass: s.DeviceClass,
}
}

View File

@ -1,12 +1,44 @@
package grpc package grpc
import ( import (
"context"
hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driving"
) )
type SwitchGRPC struct { type SwitchGRPC struct {
hav1.UnimplementedSwitchServiceServer hav1.UnimplementedSwitchServiceServer
svc driving.SwitchService
} }
// All methods return codes.Unimplemented via the embedded UnimplementedSwitchServiceServer. func NewSwitchGRPC(svc driving.SwitchService) *SwitchGRPC {
// TODO: follow the same pattern as LightGRPC once app/switch.go is implemented. return &SwitchGRPC{svc: svc}
}
func (h *SwitchGRPC) ListSwitches(ctx context.Context, req *hav1.ListSwitchesRequest) (*hav1.ListSwitchesResponse, error) {
switches, err := h.svc.ListSwitches(ctx)
if err != nil {
return nil, grpcError(err)
}
out := make([]*hav1.SwitchEntity, 0, len(switches))
for _, s := range switches {
out = append(out, domainSwitchToProto(s))
}
return &hav1.ListSwitchesResponse{Switches: out}, nil
}
// TurnOn, TurnOff, Toggle — left as Unimplemented for now.
// TODO: implement once app/switch.go has callService support.
// Follow the same pattern as LightGRPC: payload{"entity_id": ...} → ha.CallService.
func (h *SwitchGRPC) TurnOn(ctx context.Context, req *hav1.SwitchRequest) (*hav1.SwitchResponse, error) {
return nil, grpcError(ErrNotImplemented)
}
func (h *SwitchGRPC) TurnOff(ctx context.Context, req *hav1.SwitchRequest) (*hav1.SwitchResponse, error) {
return nil, grpcError(ErrNotImplemented)
}
func (h *SwitchGRPC) Toggle(ctx context.Context, req *hav1.SwitchRequest) (*hav1.SwitchResponse, error) {
return nil, grpcError(ErrNotImplemented)
}

View File

@ -2,6 +2,8 @@ package app
import ( import (
"context" "context"
"strings"
"sync"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain" "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driven" "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driven"
@ -9,12 +11,49 @@ import (
type LightApp struct { type LightApp struct {
ha driven.HAClient ha driven.HAClient
mu sync.RWMutex
cache []domain.Light
} }
func NewLightApp(ha driven.HAClient) *LightApp { func NewLightApp(ha driven.HAClient) *LightApp {
return &LightApp{ha: ha} return &LightApp{ha: ha}
} }
func (a *LightApp) Refresh(ctx context.Context) error {
all, err := a.ha.ListStates(ctx)
if err != nil {
return err
}
var lights []domain.Light
for _, s := range all {
if !strings.HasPrefix(s.EntityID, "light.") {
continue
}
lights = append(lights, haStateToLight(s))
}
a.mu.Lock()
a.cache = lights
a.mu.Unlock()
return nil
}
func (a *LightApp) ListLights(ctx context.Context) ([]domain.Light, error) {
a.mu.RLock()
c := a.cache
a.mu.RUnlock()
if c == nil {
if err := a.Refresh(ctx); err != nil {
return nil, err
}
a.mu.RLock()
c = a.cache
a.mu.RUnlock()
}
return c, nil
}
func (a *LightApp) TurnOn(ctx context.Context, p domain.TurnOnParams) (*domain.EntityState, error) { func (a *LightApp) TurnOn(ctx context.Context, p domain.TurnOnParams) (*domain.EntityState, error) {
payload := map[string]any{"entity_id": string(p.EntityID)} payload := map[string]any{"entity_id": string(p.EntityID)}
if p.BrightnessPct != nil { if p.BrightnessPct != nil {
@ -63,3 +102,39 @@ func (a *LightApp) callService(ctx context.Context, svcDomain, service string, p
} }
return haStateToDomain(s), nil return haStateToDomain(s), nil
} }
func haStateToLight(s *driven.HAState) domain.Light {
l := domain.Light{
EntityID: domain.EntityID(s.EntityID),
State: s.State,
}
if v, ok := s.Attributes["friendly_name"].(string); ok {
l.FriendlyName = v
}
if v, ok := s.Attributes["is_hue_group"].(bool); ok {
l.IsHueGroup = v
}
if v, ok := s.Attributes["min_color_temp_kelvin"].(float64); ok {
l.MinColorTempKelvin = uint32(v)
}
if v, ok := s.Attributes["max_color_temp_kelvin"].(float64); ok {
l.MaxColorTempKelvin = uint32(v)
}
if modes, ok := s.Attributes["supported_color_modes"].([]any); ok {
for _, m := range modes {
if ms, ok := m.(string); ok {
l.SupportedColorModes = append(l.SupportedColorModes, domain.ColorMode(ms))
}
}
}
if effects, ok := s.Attributes["effect_list"].([]any); ok {
for _, e := range effects {
if es, ok := e.(string); ok {
l.EffectList = append(l.EffectList, es)
}
}
}
return l
}

View File

@ -0,0 +1,69 @@
package app
import (
"context"
"strings"
"sync"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driven"
)
type SwitchApp struct {
ha driven.HAClient
mu sync.RWMutex
cache []domain.Switch
}
func NewSwitchApp(ha driven.HAClient) *SwitchApp {
return &SwitchApp{ha: ha}
}
func (a *SwitchApp) Refresh(ctx context.Context) error {
all, err := a.ha.ListStates(ctx)
if err != nil {
return err
}
var switches []domain.Switch
for _, s := range all {
if !strings.HasPrefix(s.EntityID, "switch.") {
continue
}
switches = append(switches, haStateToSwitch(s))
}
a.mu.Lock()
a.cache = switches
a.mu.Unlock()
return nil
}
func (a *SwitchApp) ListSwitches(ctx context.Context) ([]domain.Switch, error) {
a.mu.RLock()
c := a.cache
a.mu.RUnlock()
if c == nil {
if err := a.Refresh(ctx); err != nil {
return nil, err
}
a.mu.RLock()
c = a.cache
a.mu.RUnlock()
}
return c, nil
}
func haStateToSwitch(s *driven.HAState) domain.Switch {
sw := domain.Switch{
EntityID: domain.EntityID(s.EntityID),
State: s.State,
}
if v, ok := s.Attributes["friendly_name"].(string); ok {
sw.FriendlyName = v
}
if v, ok := s.Attributes["device_class"].(string); ok {
sw.DeviceClass = v
}
return sw
}

View File

@ -1,6 +1,9 @@
package domain package domain
import "time" import (
"errors"
"time"
)
type EntityID string type EntityID string
@ -11,3 +14,5 @@ type EntityState struct {
LastChanged time.Time LastChanged time.Time
LastUpdated time.Time LastUpdated time.Time
} }
var ErrNotImplemented = errors.New("not implemented")

View File

@ -1,8 +1,28 @@
package domain package domain
type ColorMode string
const (
ColorModeColorTemp ColorMode = "color_temp"
ColorModeHS ColorMode = "hs"
ColorModeXY ColorMode = "xy"
ColorModeBrightness ColorMode = "brightness"
)
type Light struct {
EntityID EntityID
FriendlyName string
State string // "on" | "off" | "unavailable"
SupportedColorModes []ColorMode
MinColorTempKelvin uint32
MaxColorTempKelvin uint32
IsHueGroup bool
EffectList []string
}
type TurnOnParams struct { type TurnOnParams struct {
EntityID EntityID EntityID EntityID
BrightnessPct *uint32 // nil = not set BrightnessPct *uint32
ColorTempKelvin *uint32 ColorTempKelvin *uint32
RGBColor *RGBColor RGBColor *RGBColor
Transition *uint32 Transition *uint32

View File

@ -0,0 +1,8 @@
package domain
type Switch struct {
EntityID EntityID
FriendlyName string
State string // "on" | "off" | "unavailable"
DeviceClass string // e.g. "switch"
}

View File

@ -10,4 +10,6 @@ type LightService interface {
TurnOn(ctx context.Context, p domain.TurnOnParams) (*domain.EntityState, error) TurnOn(ctx context.Context, p domain.TurnOnParams) (*domain.EntityState, error)
TurnOff(ctx context.Context, p domain.TurnOffParams) (*domain.EntityState, error) TurnOff(ctx context.Context, p domain.TurnOffParams) (*domain.EntityState, error)
Toggle(ctx context.Context, id domain.EntityID) (*domain.EntityState, error) Toggle(ctx context.Context, id domain.EntityID) (*domain.EntityState, error)
ListLights(ctx context.Context) ([]domain.Light, error)
Refresh(ctx context.Context) error
} }

View File

@ -0,0 +1,12 @@
package driving
import (
"context"
"gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain"
)
type SwitchService interface {
ListSwitches(ctx context.Context) ([]domain.Switch, error)
Refresh(ctx context.Context) error
}

View File

@ -7,6 +7,7 @@ service LightService {
rpc TurnOn(TurnOnRequest) returns (LightResponse); rpc TurnOn(TurnOnRequest) returns (LightResponse);
rpc TurnOff(TurnOffRequest) returns (LightResponse); rpc TurnOff(TurnOffRequest) returns (LightResponse);
rpc Toggle(ToggleRequest) returns (LightResponse); rpc Toggle(ToggleRequest) returns (LightResponse);
rpc ListLights(ListLightsRequest) returns (ListLightsResponse);
} }
// optional fields require protobuf 3.15+ / buf >= 1.0. They generate // optional fields require protobuf 3.15+ / buf >= 1.0. They generate
@ -25,3 +26,19 @@ message TurnOffRequest {
} }
message ToggleRequest { string entity_id = 1; } message ToggleRequest { string entity_id = 1; }
message LightResponse { EntityState state = 1; } message LightResponse { EntityState state = 1; }
message LightEntity {
string entity_id = 1;
string friendly_name = 2;
string state = 3;
repeated string supported_color_modes = 4;
uint32 min_color_temp_kelvin = 5;
uint32 max_color_temp_kelvin = 6;
bool is_hue_group = 7;
repeated string effect_list = 8;
}
message ListLightsRequest {}
message ListLightsResponse {
repeated LightEntity lights = 1;
}

View File

@ -3,16 +3,25 @@ package ha.v1;
option go_package = "gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1"; option go_package = "gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1";
import "ha/v1/common.proto"; import "ha/v1/common.proto";
// TODO: implement SwitchService. Follow the same pattern as LightService:
// - domain type in core/domain/switch.go
// - port interface in core/ports/driving/switch.go
// - application logic in app/switch.go
// - gRPC adapter in adapters/primary/grpc/switch.go
service SwitchService { service SwitchService {
rpc TurnOn(SwitchRequest) returns (SwitchResponse); rpc TurnOn(SwitchRequest) returns (SwitchResponse);
rpc TurnOff(SwitchRequest) returns (SwitchResponse); rpc TurnOff(SwitchRequest) returns (SwitchResponse);
rpc Toggle(SwitchRequest) returns (SwitchResponse); rpc Toggle(SwitchRequest) returns (SwitchResponse);
rpc ListSwitches(ListSwitchesRequest) returns (ListSwitchesResponse);
} }
message SwitchRequest { string entity_id = 1; } message SwitchRequest { string entity_id = 1; }
message SwitchResponse { EntityState state = 1; } message SwitchResponse { EntityState state = 1; }
message SwitchEntity {
string entity_id = 1;
string friendly_name = 2;
string state = 3;
string device_class = 4;
}
message ListSwitchesRequest {}
message ListSwitchesResponse {
repeated SwitchEntity switches = 1;
}