diff --git a/README.md b/README.md index e69de29..90fa31c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/gen/ha/v1/light.pb.go b/gen/ha/v1/light.pb.go index 32eea95..d50fd11 100644 --- a/gen/ha/v1/light.pb.go +++ b/gen/ha/v1/light.pb.go @@ -240,6 +240,186 @@ func (x *LightResponse) GetState() *EntityState { 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 const file_ha_v1_light_proto_rawDesc = "" + @@ -267,11 +447,27 @@ const file_ha_v1_light_proto_rawDesc = "" + "\rToggleRequest\x12\x1b\n" + "\tentity_id\x18\x01 \x01(\tR\bentityId\"9\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" + "\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" + - "\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 ( 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 } -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{ - (*TurnOnRequest)(nil), // 0: ha.v1.TurnOnRequest - (*TurnOffRequest)(nil), // 1: ha.v1.TurnOffRequest - (*ToggleRequest)(nil), // 2: ha.v1.ToggleRequest - (*LightResponse)(nil), // 3: ha.v1.LightResponse - (*RGBColor)(nil), // 4: ha.v1.RGBColor - (*EntityState)(nil), // 5: ha.v1.EntityState + (*TurnOnRequest)(nil), // 0: ha.v1.TurnOnRequest + (*TurnOffRequest)(nil), // 1: ha.v1.TurnOffRequest + (*ToggleRequest)(nil), // 2: ha.v1.ToggleRequest + (*LightResponse)(nil), // 3: ha.v1.LightResponse + (*LightEntity)(nil), // 4: ha.v1.LightEntity + (*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{ - 4, // 0: ha.v1.TurnOnRequest.rgb_color:type_name -> ha.v1.RGBColor - 5, // 1: ha.v1.LightResponse.state:type_name -> ha.v1.EntityState - 0, // 2: ha.v1.LightService.TurnOn:input_type -> ha.v1.TurnOnRequest - 1, // 3: ha.v1.LightService.TurnOff:input_type -> ha.v1.TurnOffRequest - 2, // 4: ha.v1.LightService.Toggle:input_type -> ha.v1.ToggleRequest - 3, // 5: ha.v1.LightService.TurnOn:output_type -> ha.v1.LightResponse - 3, // 6: ha.v1.LightService.TurnOff:output_type -> ha.v1.LightResponse - 3, // 7: ha.v1.LightService.Toggle:output_type -> ha.v1.LightResponse - 5, // [5:8] is the sub-list for method output_type - 2, // [2:5] 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 + 7, // 0: ha.v1.TurnOnRequest.rgb_color:type_name -> ha.v1.RGBColor + 8, // 1: ha.v1.LightResponse.state:type_name -> ha.v1.EntityState + 4, // 2: ha.v1.ListLightsResponse.lights:type_name -> ha.v1.LightEntity + 0, // 3: ha.v1.LightService.TurnOn:input_type -> ha.v1.TurnOnRequest + 1, // 4: ha.v1.LightService.TurnOff:input_type -> ha.v1.TurnOffRequest + 2, // 5: ha.v1.LightService.Toggle:input_type -> ha.v1.ToggleRequest + 5, // 6: ha.v1.LightService.ListLights:input_type -> ha.v1.ListLightsRequest + 3, // 7: ha.v1.LightService.TurnOn:output_type -> ha.v1.LightResponse + 3, // 8: ha.v1.LightService.TurnOff:output_type -> ha.v1.LightResponse + 3, // 9: ha.v1.LightService.Toggle:output_type -> ha.v1.LightResponse + 6, // 10: ha.v1.LightService.ListLights:output_type -> ha.v1.ListLightsResponse + 7, // [7:11] is the sub-list for method output_type + 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() } @@ -324,7 +526,7 @@ func file_ha_v1_light_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_light_proto_rawDesc), len(file_ha_v1_light_proto_rawDesc)), NumEnums: 0, - NumMessages: 4, + NumMessages: 7, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/ha/v1/light_grpc.pb.go b/gen/ha/v1/light_grpc.pb.go index 7ae446d..01c5c4c 100644 --- a/gen/ha/v1/light_grpc.pb.go +++ b/gen/ha/v1/light_grpc.pb.go @@ -19,9 +19,10 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - LightService_TurnOn_FullMethodName = "/ha.v1.LightService/TurnOn" - LightService_TurnOff_FullMethodName = "/ha.v1.LightService/TurnOff" - LightService_Toggle_FullMethodName = "/ha.v1.LightService/Toggle" + LightService_TurnOn_FullMethodName = "/ha.v1.LightService/TurnOn" + LightService_TurnOff_FullMethodName = "/ha.v1.LightService/TurnOff" + LightService_Toggle_FullMethodName = "/ha.v1.LightService/Toggle" + LightService_ListLights_FullMethodName = "/ha.v1.LightService/ListLights" ) // 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) TurnOff(ctx context.Context, in *TurnOffRequest, 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 { @@ -71,6 +73,16 @@ func (c *lightServiceClient) Toggle(ctx context.Context, in *ToggleRequest, opts 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. // All implementations must embed UnimplementedLightServiceServer // for forward compatibility. @@ -78,6 +90,7 @@ type LightServiceServer interface { TurnOn(context.Context, *TurnOnRequest) (*LightResponse, error) TurnOff(context.Context, *TurnOffRequest) (*LightResponse, error) Toggle(context.Context, *ToggleRequest) (*LightResponse, error) + ListLights(context.Context, *ListLightsRequest) (*ListLightsResponse, error) mustEmbedUnimplementedLightServiceServer() } @@ -97,6 +110,9 @@ func (UnimplementedLightServiceServer) TurnOff(context.Context, *TurnOffRequest) func (UnimplementedLightServiceServer) Toggle(context.Context, *ToggleRequest) (*LightResponse, error) { 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) testEmbeddedByValue() {} @@ -172,6 +188,24 @@ func _LightService_Toggle_Handler(srv interface{}, ctx context.Context, dec func 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. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -191,6 +225,10 @@ var LightService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Toggle", Handler: _LightService_Toggle_Handler, }, + { + MethodName: "ListLights", + Handler: _LightService_ListLights_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "ha/v1/light.proto", diff --git a/gen/ha/v1/switch.pb.go b/gen/ha/v1/switch.pb.go index ec9c635..285ddc1 100644 --- a/gen/ha/v1/switch.pb.go +++ b/gen/ha/v1/switch.pb.go @@ -109,6 +109,154 @@ func (x *SwitchResponse) GetState() *EntityState { 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 const file_ha_v1_switch_proto_rawDesc = "" + @@ -117,11 +265,20 @@ const file_ha_v1_switch_proto_rawDesc = "" + "\rSwitchRequest\x12\x1b\n" + "\tentity_id\x18\x01 \x01(\tR\bentityId\":\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" + "\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" + - "\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 ( 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 } -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{ - (*SwitchRequest)(nil), // 0: ha.v1.SwitchRequest - (*SwitchResponse)(nil), // 1: ha.v1.SwitchResponse - (*EntityState)(nil), // 2: ha.v1.EntityState + (*SwitchRequest)(nil), // 0: ha.v1.SwitchRequest + (*SwitchResponse)(nil), // 1: ha.v1.SwitchResponse + (*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{ - 2, // 0: ha.v1.SwitchResponse.state:type_name -> ha.v1.EntityState - 0, // 1: ha.v1.SwitchService.TurnOn:input_type -> ha.v1.SwitchRequest - 0, // 2: ha.v1.SwitchService.TurnOff:input_type -> ha.v1.SwitchRequest - 0, // 3: ha.v1.SwitchService.Toggle:input_type -> ha.v1.SwitchRequest - 1, // 4: ha.v1.SwitchService.TurnOn:output_type -> ha.v1.SwitchResponse - 1, // 5: ha.v1.SwitchService.TurnOff:output_type -> ha.v1.SwitchResponse - 1, // 6: ha.v1.SwitchService.Toggle:output_type -> ha.v1.SwitchResponse - 4, // [4:7] is the sub-list for method output_type - 1, // [1:4] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 5, // 0: ha.v1.SwitchResponse.state:type_name -> ha.v1.EntityState + 2, // 1: ha.v1.ListSwitchesResponse.switches:type_name -> ha.v1.SwitchEntity + 0, // 2: ha.v1.SwitchService.TurnOn:input_type -> ha.v1.SwitchRequest + 0, // 3: ha.v1.SwitchService.TurnOff:input_type -> ha.v1.SwitchRequest + 0, // 4: ha.v1.SwitchService.Toggle:input_type -> ha.v1.SwitchRequest + 3, // 5: ha.v1.SwitchService.ListSwitches:input_type -> ha.v1.ListSwitchesRequest + 1, // 6: ha.v1.SwitchService.TurnOn:output_type -> ha.v1.SwitchResponse + 1, // 7: ha.v1.SwitchService.TurnOff:output_type -> ha.v1.SwitchResponse + 1, // 8: ha.v1.SwitchService.Toggle:output_type -> ha.v1.SwitchResponse + 4, // 9: ha.v1.SwitchService.ListSwitches:output_type -> ha.v1.ListSwitchesResponse + 6, // [6:10] is the sub-list for method output_type + 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() } @@ -168,7 +331,7 @@ func file_ha_v1_switch_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_switch_proto_rawDesc), len(file_ha_v1_switch_proto_rawDesc)), NumEnums: 0, - NumMessages: 2, + NumMessages: 5, NumExtensions: 0, NumServices: 1, }, diff --git a/gen/ha/v1/switch_grpc.pb.go b/gen/ha/v1/switch_grpc.pb.go index 806bc0e..a89c523 100644 --- a/gen/ha/v1/switch_grpc.pb.go +++ b/gen/ha/v1/switch_grpc.pb.go @@ -19,24 +19,20 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - SwitchService_TurnOn_FullMethodName = "/ha.v1.SwitchService/TurnOn" - SwitchService_TurnOff_FullMethodName = "/ha.v1.SwitchService/TurnOff" - SwitchService_Toggle_FullMethodName = "/ha.v1.SwitchService/Toggle" + SwitchService_TurnOn_FullMethodName = "/ha.v1.SwitchService/TurnOn" + SwitchService_TurnOff_FullMethodName = "/ha.v1.SwitchService/TurnOff" + SwitchService_Toggle_FullMethodName = "/ha.v1.SwitchService/Toggle" + SwitchService_ListSwitches_FullMethodName = "/ha.v1.SwitchService/ListSwitches" ) // 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. -// -// 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 { TurnOn(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) + ListSwitches(ctx context.Context, in *ListSwitchesRequest, opts ...grpc.CallOption) (*ListSwitchesResponse, error) } type switchServiceClient struct { @@ -77,19 +73,24 @@ func (c *switchServiceClient) Toggle(ctx context.Context, in *SwitchRequest, opt 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. // All implementations must embed UnimplementedSwitchServiceServer // 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 { TurnOn(context.Context, *SwitchRequest) (*SwitchResponse, error) TurnOff(context.Context, *SwitchRequest) (*SwitchResponse, error) Toggle(context.Context, *SwitchRequest) (*SwitchResponse, error) + ListSwitches(context.Context, *ListSwitchesRequest) (*ListSwitchesResponse, error) mustEmbedUnimplementedSwitchServiceServer() } @@ -109,6 +110,9 @@ func (UnimplementedSwitchServiceServer) TurnOff(context.Context, *SwitchRequest) func (UnimplementedSwitchServiceServer) Toggle(context.Context, *SwitchRequest) (*SwitchResponse, error) { 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) testEmbeddedByValue() {} @@ -184,6 +188,24 @@ func _SwitchService_Toggle_Handler(srv interface{}, ctx context.Context, dec fun 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. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -203,6 +225,10 @@ var SwitchService_ServiceDesc = grpc.ServiceDesc{ MethodName: "Toggle", Handler: _SwitchService_Toggle_Handler, }, + { + MethodName: "ListSwitches", + Handler: _SwitchService_ListSwitches_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "ha/v1/switch.proto", diff --git a/go.work.sum b/go.work.sum index 439f9d5..6eed3c9 100644 --- a/go.work.sum +++ b/go.work.sum @@ -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= 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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 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/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/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= @@ -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.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA= 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/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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/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/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= -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/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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/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/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/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/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.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/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/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 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/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 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/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/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 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/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 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/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= 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-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/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= 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.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= diff --git a/ha-gateway/cmd/gateway/main.go b/ha-gateway/cmd/gateway/main.go index 0ae9ab7..3f6c9df 100644 --- a/ha-gateway/cmd/gateway/main.go +++ b/ha-gateway/cmd/gateway/main.go @@ -10,13 +10,13 @@ import ( "time" "github.com/joho/godotenv" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" "google.golang.org/grpc" "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" - "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" + "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/config" "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/telemetry" @@ -61,6 +61,16 @@ func main() { // 5. Build application services. entityApp := app.NewEntityApp(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. srv := grpc.NewServer( @@ -72,7 +82,7 @@ func main() { // 7. Register services. hav1.RegisterEntityServiceServer(srv, grpcadapter.NewEntityGRPC(entityApp)) hav1.RegisterLightServiceServer(srv, grpcadapter.NewLightGRPC(lightApp)) - hav1.RegisterSwitchServiceServer(srv, &grpcadapter.SwitchGRPC{}) + hav1.RegisterSwitchServiceServer(srv, grpcadapter.NewSwitchGRPC(switchApp)) hav1.RegisterEventServiceServer(srv, &grpcadapter.EventGRPC{}) // 8. Start listener. diff --git a/ha-gateway/internal/adapters/primary/grpc/entity.go b/ha-gateway/internal/adapters/primary/grpc/entity.go index 20c45fc..1ee61c2 100644 --- a/ha-gateway/internal/adapters/primary/grpc/entity.go +++ b/ha-gateway/internal/adapters/primary/grpc/entity.go @@ -52,8 +52,14 @@ func grpcError(err error) error { if errors.Is(err, ErrNotFound) { 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) } // ErrNotFound is returned by the app layer when an entity does not exist. var ErrNotFound = errors.New("not found") + +// ErrNotImplemented is returned by handlers that are stubbed pending full implementation. +var ErrNotImplemented = errors.New("not implemented") diff --git a/ha-gateway/internal/adapters/primary/grpc/light.go b/ha-gateway/internal/adapters/primary/grpc/light.go index be96385..1c7e0b4 100644 --- a/ha-gateway/internal/adapters/primary/grpc/light.go +++ b/ha-gateway/internal/adapters/primary/grpc/light.go @@ -40,3 +40,15 @@ func (h *LightGRPC) Toggle(ctx context.Context, req *hav1.ToggleRequest) (*hav1. } 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 +} diff --git a/ha-gateway/internal/adapters/primary/grpc/mapping.go b/ha-gateway/internal/adapters/primary/grpc/mapping.go index 8e7755e..b272040 100644 --- a/ha-gateway/internal/adapters/primary/grpc/mapping.go +++ b/ha-gateway/internal/adapters/primary/grpc/mapping.go @@ -1,8 +1,8 @@ package grpc import ( - "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain" 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 { @@ -51,3 +51,29 @@ func protoTurnOffToParams(r *hav1.TurnOffRequest) domain.TurnOffParams { } 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, + } +} diff --git a/ha-gateway/internal/adapters/primary/grpc/switch.go b/ha-gateway/internal/adapters/primary/grpc/switch.go index 2b2e559..677c9b4 100644 --- a/ha-gateway/internal/adapters/primary/grpc/switch.go +++ b/ha-gateway/internal/adapters/primary/grpc/switch.go @@ -1,12 +1,44 @@ package grpc import ( + "context" + 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 { hav1.UnimplementedSwitchServiceServer + svc driving.SwitchService } -// All methods return codes.Unimplemented via the embedded UnimplementedSwitchServiceServer. -// TODO: follow the same pattern as LightGRPC once app/switch.go is implemented. +func NewSwitchGRPC(svc driving.SwitchService) *SwitchGRPC { + 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) +} diff --git a/ha-gateway/internal/app/light.go b/ha-gateway/internal/app/light.go index 11dd2d6..bbb0f4d 100644 --- a/ha-gateway/internal/app/light.go +++ b/ha-gateway/internal/app/light.go @@ -2,19 +2,58 @@ 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 LightApp struct { - ha driven.HAClient + ha driven.HAClient + mu sync.RWMutex + cache []domain.Light } func NewLightApp(ha driven.HAClient) *LightApp { 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) { payload := map[string]any{"entity_id": string(p.EntityID)} if p.BrightnessPct != nil { @@ -63,3 +102,39 @@ func (a *LightApp) callService(ctx context.Context, svcDomain, service string, p } 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 +} diff --git a/ha-gateway/internal/app/switch.go b/ha-gateway/internal/app/switch.go new file mode 100644 index 0000000..b8b942f --- /dev/null +++ b/ha-gateway/internal/app/switch.go @@ -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 +} diff --git a/ha-gateway/internal/core/domain/entity.go b/ha-gateway/internal/core/domain/entity.go index b9fed0c..3caf677 100644 --- a/ha-gateway/internal/core/domain/entity.go +++ b/ha-gateway/internal/core/domain/entity.go @@ -1,6 +1,9 @@ package domain -import "time" +import ( + "errors" + "time" +) type EntityID string @@ -11,3 +14,5 @@ type EntityState struct { LastChanged time.Time LastUpdated time.Time } + +var ErrNotImplemented = errors.New("not implemented") diff --git a/ha-gateway/internal/core/domain/light.go b/ha-gateway/internal/core/domain/light.go index 7c36b09..9384745 100644 --- a/ha-gateway/internal/core/domain/light.go +++ b/ha-gateway/internal/core/domain/light.go @@ -1,8 +1,28 @@ 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 { EntityID EntityID - BrightnessPct *uint32 // nil = not set + BrightnessPct *uint32 ColorTempKelvin *uint32 RGBColor *RGBColor Transition *uint32 diff --git a/ha-gateway/internal/core/domain/switch.go b/ha-gateway/internal/core/domain/switch.go new file mode 100644 index 0000000..3357e18 --- /dev/null +++ b/ha-gateway/internal/core/domain/switch.go @@ -0,0 +1,8 @@ +package domain + +type Switch struct { + EntityID EntityID + FriendlyName string + State string // "on" | "off" | "unavailable" + DeviceClass string // e.g. "switch" +} diff --git a/ha-gateway/internal/core/ports/driving/light.go b/ha-gateway/internal/core/ports/driving/light.go index 3d22410..99364b9 100644 --- a/ha-gateway/internal/core/ports/driving/light.go +++ b/ha-gateway/internal/core/ports/driving/light.go @@ -10,4 +10,6 @@ type LightService interface { TurnOn(ctx context.Context, p domain.TurnOnParams) (*domain.EntityState, error) TurnOff(ctx context.Context, p domain.TurnOffParams) (*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 } diff --git a/ha-gateway/internal/core/ports/driving/switch.go b/ha-gateway/internal/core/ports/driving/switch.go new file mode 100644 index 0000000..22fff89 --- /dev/null +++ b/ha-gateway/internal/core/ports/driving/switch.go @@ -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 +} diff --git a/proto/ha/v1/light.proto b/proto/ha/v1/light.proto index 2d59290..914f5c3 100644 --- a/proto/ha/v1/light.proto +++ b/proto/ha/v1/light.proto @@ -7,6 +7,7 @@ service LightService { rpc TurnOn(TurnOnRequest) returns (LightResponse); rpc TurnOff(TurnOffRequest) returns (LightResponse); rpc Toggle(ToggleRequest) returns (LightResponse); + rpc ListLights(ListLightsRequest) returns (ListLightsResponse); } // optional fields require protobuf 3.15+ / buf >= 1.0. They generate @@ -25,3 +26,19 @@ message TurnOffRequest { } message ToggleRequest { string entity_id = 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; +} diff --git a/proto/ha/v1/switch.proto b/proto/ha/v1/switch.proto index 4d38d80..8fb6582 100644 --- a/proto/ha/v1/switch.proto +++ b/proto/ha/v1/switch.proto @@ -3,16 +3,25 @@ package ha.v1; option go_package = "gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1"; 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 { - rpc TurnOn(SwitchRequest) returns (SwitchResponse); - rpc TurnOff(SwitchRequest) returns (SwitchResponse); - rpc Toggle(SwitchRequest) returns (SwitchResponse); + rpc TurnOn(SwitchRequest) returns (SwitchResponse); + rpc TurnOff(SwitchRequest) returns (SwitchResponse); + rpc Toggle(SwitchRequest) returns (SwitchResponse); + rpc ListSwitches(ListSwitchesRequest) returns (ListSwitchesResponse); } message SwitchRequest { string entity_id = 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; +} \ No newline at end of file