From 2e99c464ffde81779977a873db148e61352e80d2 Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Wed, 25 Mar 2026 19:52:15 +0900 Subject: [PATCH] Added new app, ha-gateway --- buf.gen.yaml | 10 + buf.yaml | 3 + gen/go.mod | 15 + gen/go.sum | 38 ++ gen/ha/v1/common.pb.go | 230 ++++++++++++ gen/ha/v1/entity.pb.go | 285 +++++++++++++++ gen/ha/v1/entity_grpc.pb.go | 159 ++++++++ gen/ha/v1/event.pb.go | 218 +++++++++++ gen/ha/v1/event_grpc.pb.go | 144 ++++++++ gen/ha/v1/light.pb.go | 338 ++++++++++++++++++ gen/ha/v1/light_grpc.pb.go | 197 ++++++++++ gen/ha/v1/switch.pb.go | 182 ++++++++++ gen/ha/v1/switch_grpc.pb.go | 209 +++++++++++ go.work | 6 + go.work.sum | 92 +++++ ha-gateway/.env.example | 4 + ha-gateway/Dockerfile | 19 + ha-gateway/cmd/gateway/main.go | 143 ++++++++ ha-gateway/go.mod | 37 ++ ha-gateway/go.sum | 67 ++++ .../internal/adapters/primary/grpc/entity.go | 59 +++ .../internal/adapters/primary/grpc/event.go | 12 + .../internal/adapters/primary/grpc/light.go | 42 +++ .../internal/adapters/primary/grpc/mapping.go | 53 +++ .../internal/adapters/primary/grpc/switch.go | 12 + .../internal/adapters/secondary/ha/client.go | 183 ++++++++++ .../adapters/secondary/ha/websocket.go | 7 + ha-gateway/internal/app/entity.go | 68 ++++ ha-gateway/internal/app/light.go | 65 ++++ ha-gateway/internal/config/config.go | 33 ++ ha-gateway/internal/core/domain/entity.go | 13 + ha-gateway/internal/core/domain/light.go | 18 + ha-gateway/internal/core/ports/driven/ha.go | 22 ++ .../internal/core/ports/driving/entity.go | 12 + .../internal/core/ports/driving/light.go | 13 + ha-gateway/internal/telemetry/telemetry.go | 78 ++++ proto/ha/v1/common.proto | 17 + proto/ha/v1/entity.proto | 18 + proto/ha/v1/event.proto | 29 ++ proto/ha/v1/light.proto | 27 ++ proto/ha/v1/switch.proto | 18 + 41 files changed, 3195 insertions(+) create mode 100644 buf.gen.yaml create mode 100644 buf.yaml create mode 100644 gen/go.mod create mode 100644 gen/go.sum create mode 100644 gen/ha/v1/common.pb.go create mode 100644 gen/ha/v1/entity.pb.go create mode 100644 gen/ha/v1/entity_grpc.pb.go create mode 100644 gen/ha/v1/event.pb.go create mode 100644 gen/ha/v1/event_grpc.pb.go create mode 100644 gen/ha/v1/light.pb.go create mode 100644 gen/ha/v1/light_grpc.pb.go create mode 100644 gen/ha/v1/switch.pb.go create mode 100644 gen/ha/v1/switch_grpc.pb.go create mode 100644 go.work create mode 100644 go.work.sum create mode 100644 ha-gateway/.env.example create mode 100644 ha-gateway/Dockerfile create mode 100644 ha-gateway/cmd/gateway/main.go create mode 100644 ha-gateway/go.mod create mode 100644 ha-gateway/go.sum create mode 100644 ha-gateway/internal/adapters/primary/grpc/entity.go create mode 100644 ha-gateway/internal/adapters/primary/grpc/event.go create mode 100644 ha-gateway/internal/adapters/primary/grpc/light.go create mode 100644 ha-gateway/internal/adapters/primary/grpc/mapping.go create mode 100644 ha-gateway/internal/adapters/primary/grpc/switch.go create mode 100644 ha-gateway/internal/adapters/secondary/ha/client.go create mode 100644 ha-gateway/internal/adapters/secondary/ha/websocket.go create mode 100644 ha-gateway/internal/app/entity.go create mode 100644 ha-gateway/internal/app/light.go create mode 100644 ha-gateway/internal/config/config.go create mode 100644 ha-gateway/internal/core/domain/entity.go create mode 100644 ha-gateway/internal/core/domain/light.go create mode 100644 ha-gateway/internal/core/ports/driven/ha.go create mode 100644 ha-gateway/internal/core/ports/driving/entity.go create mode 100644 ha-gateway/internal/core/ports/driving/light.go create mode 100644 ha-gateway/internal/telemetry/telemetry.go create mode 100644 proto/ha/v1/common.proto create mode 100644 proto/ha/v1/entity.proto create mode 100644 proto/ha/v1/event.proto create mode 100644 proto/ha/v1/light.proto create mode 100644 proto/ha/v1/switch.proto diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 0000000..42ce5e7 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,10 @@ +version: v2 +plugins: + - remote: buf.build/protocolbuffers/go + out: gen + opt: + - paths=source_relative + - remote: buf.build/grpc/go + out: gen + opt: + - paths=source_relative diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 0000000..ba5ddf5 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,3 @@ +version: v2 +modules: + - path: proto diff --git a/gen/go.mod b/gen/go.mod new file mode 100644 index 0000000..64ed443 --- /dev/null +++ b/gen/go.mod @@ -0,0 +1,15 @@ +module gitea.nik4nao.com/nik/home-services/gen + +go 1.26 + +require ( + google.golang.org/grpc v1.79.3 + google.golang.org/protobuf v1.36.11 +) + +require ( + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect +) diff --git a/gen/go.sum b/gen/go.sum new file mode 100644 index 0000000..d705bc9 --- /dev/null +++ b/gen/go.sum @@ -0,0 +1,38 @@ +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/gen/ha/v1/common.pb.go b/gen/ha/v1/common.pb.go new file mode 100644 index 0000000..f58a3e7 --- /dev/null +++ b/gen/ha/v1/common.pb.go @@ -0,0 +1,230 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ha/v1/common.proto + +package hav1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EntityState struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + State string `protobuf:"bytes,2,opt,name=state,proto3" json:"state,omitempty"` + Attributes map[string]string `protobuf:"bytes,3,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + LastChanged string `protobuf:"bytes,4,opt,name=last_changed,json=lastChanged,proto3" json:"last_changed,omitempty"` // RFC3339 + LastUpdated string `protobuf:"bytes,5,opt,name=last_updated,json=lastUpdated,proto3" json:"last_updated,omitempty"` // RFC3339 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EntityState) Reset() { + *x = EntityState{} + mi := &file_ha_v1_common_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EntityState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EntityState) ProtoMessage() {} + +func (x *EntityState) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_common_proto_msgTypes[0] + 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 EntityState.ProtoReflect.Descriptor instead. +func (*EntityState) Descriptor() ([]byte, []int) { + return file_ha_v1_common_proto_rawDescGZIP(), []int{0} +} + +func (x *EntityState) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +func (x *EntityState) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *EntityState) GetAttributes() map[string]string { + if x != nil { + return x.Attributes + } + return nil +} + +func (x *EntityState) GetLastChanged() string { + if x != nil { + return x.LastChanged + } + return "" +} + +func (x *EntityState) GetLastUpdated() string { + if x != nil { + return x.LastUpdated + } + return "" +} + +type RGBColor struct { + state protoimpl.MessageState `protogen:"open.v1"` + R uint32 `protobuf:"varint,1,opt,name=r,proto3" json:"r,omitempty"` + G uint32 `protobuf:"varint,2,opt,name=g,proto3" json:"g,omitempty"` + B uint32 `protobuf:"varint,3,opt,name=b,proto3" json:"b,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RGBColor) Reset() { + *x = RGBColor{} + mi := &file_ha_v1_common_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RGBColor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RGBColor) ProtoMessage() {} + +func (x *RGBColor) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_common_proto_msgTypes[1] + 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 RGBColor.ProtoReflect.Descriptor instead. +func (*RGBColor) Descriptor() ([]byte, []int) { + return file_ha_v1_common_proto_rawDescGZIP(), []int{1} +} + +func (x *RGBColor) GetR() uint32 { + if x != nil { + return x.R + } + return 0 +} + +func (x *RGBColor) GetG() uint32 { + if x != nil { + return x.G + } + return 0 +} + +func (x *RGBColor) GetB() uint32 { + if x != nil { + return x.B + } + return 0 +} + +var File_ha_v1_common_proto protoreflect.FileDescriptor + +const file_ha_v1_common_proto_rawDesc = "" + + "\n" + + "\x12ha/v1/common.proto\x12\x05ha.v1\"\x89\x02\n" + + "\vEntityState\x12\x1b\n" + + "\tentity_id\x18\x01 \x01(\tR\bentityId\x12\x14\n" + + "\x05state\x18\x02 \x01(\tR\x05state\x12B\n" + + "\n" + + "attributes\x18\x03 \x03(\v2\".ha.v1.EntityState.AttributesEntryR\n" + + "attributes\x12!\n" + + "\flast_changed\x18\x04 \x01(\tR\vlastChanged\x12!\n" + + "\flast_updated\x18\x05 \x01(\tR\vlastUpdated\x1a=\n" + + "\x0fAttributesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"4\n" + + "\bRGBColor\x12\f\n" + + "\x01r\x18\x01 \x01(\rR\x01r\x12\f\n" + + "\x01g\x18\x02 \x01(\rR\x01g\x12\f\n" + + "\x01b\x18\x03 \x01(\rR\x01bB4Z2gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1b\x06proto3" + +var ( + file_ha_v1_common_proto_rawDescOnce sync.Once + file_ha_v1_common_proto_rawDescData []byte +) + +func file_ha_v1_common_proto_rawDescGZIP() []byte { + file_ha_v1_common_proto_rawDescOnce.Do(func() { + file_ha_v1_common_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ha_v1_common_proto_rawDesc), len(file_ha_v1_common_proto_rawDesc))) + }) + return file_ha_v1_common_proto_rawDescData +} + +var file_ha_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_ha_v1_common_proto_goTypes = []any{ + (*EntityState)(nil), // 0: ha.v1.EntityState + (*RGBColor)(nil), // 1: ha.v1.RGBColor + nil, // 2: ha.v1.EntityState.AttributesEntry +} +var file_ha_v1_common_proto_depIdxs = []int32{ + 2, // 0: ha.v1.EntityState.attributes:type_name -> ha.v1.EntityState.AttributesEntry + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] 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 +} + +func init() { file_ha_v1_common_proto_init() } +func file_ha_v1_common_proto_init() { + if File_ha_v1_common_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_common_proto_rawDesc), len(file_ha_v1_common_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_ha_v1_common_proto_goTypes, + DependencyIndexes: file_ha_v1_common_proto_depIdxs, + MessageInfos: file_ha_v1_common_proto_msgTypes, + }.Build() + File_ha_v1_common_proto = out.File + file_ha_v1_common_proto_goTypes = nil + file_ha_v1_common_proto_depIdxs = nil +} diff --git a/gen/ha/v1/entity.pb.go b/gen/ha/v1/entity.pb.go new file mode 100644 index 0000000..e717da7 --- /dev/null +++ b/gen/ha/v1/entity.pb.go @@ -0,0 +1,285 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ha/v1/entity.proto + +package hav1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStateRequest) Reset() { + *x = GetStateRequest{} + mi := &file_ha_v1_entity_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStateRequest) ProtoMessage() {} + +func (x *GetStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_entity_proto_msgTypes[0] + 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 GetStateRequest.ProtoReflect.Descriptor instead. +func (*GetStateRequest) Descriptor() ([]byte, []int) { + return file_ha_v1_entity_proto_rawDescGZIP(), []int{0} +} + +func (x *GetStateRequest) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +type GetStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State *EntityState `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetStateResponse) Reset() { + *x = GetStateResponse{} + mi := &file_ha_v1_entity_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetStateResponse) ProtoMessage() {} + +func (x *GetStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_entity_proto_msgTypes[1] + 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 GetStateResponse.ProtoReflect.Descriptor instead. +func (*GetStateResponse) Descriptor() ([]byte, []int) { + return file_ha_v1_entity_proto_rawDescGZIP(), []int{1} +} + +func (x *GetStateResponse) GetState() *EntityState { + if x != nil { + return x.State + } + return nil +} + +type ListStatesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityIds []string `protobuf:"bytes,1,rep,name=entity_ids,json=entityIds,proto3" json:"entity_ids,omitempty"` + Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListStatesRequest) Reset() { + *x = ListStatesRequest{} + mi := &file_ha_v1_entity_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListStatesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStatesRequest) ProtoMessage() {} + +func (x *ListStatesRequest) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_entity_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 ListStatesRequest.ProtoReflect.Descriptor instead. +func (*ListStatesRequest) Descriptor() ([]byte, []int) { + return file_ha_v1_entity_proto_rawDescGZIP(), []int{2} +} + +func (x *ListStatesRequest) GetEntityIds() []string { + if x != nil { + return x.EntityIds + } + return nil +} + +func (x *ListStatesRequest) GetDomain() string { + if x != nil { + return x.Domain + } + return "" +} + +type ListStatesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + States []*EntityState `protobuf:"bytes,1,rep,name=states,proto3" json:"states,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListStatesResponse) Reset() { + *x = ListStatesResponse{} + mi := &file_ha_v1_entity_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListStatesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListStatesResponse) ProtoMessage() {} + +func (x *ListStatesResponse) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_entity_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 ListStatesResponse.ProtoReflect.Descriptor instead. +func (*ListStatesResponse) Descriptor() ([]byte, []int) { + return file_ha_v1_entity_proto_rawDescGZIP(), []int{3} +} + +func (x *ListStatesResponse) GetStates() []*EntityState { + if x != nil { + return x.States + } + return nil +} + +var File_ha_v1_entity_proto protoreflect.FileDescriptor + +const file_ha_v1_entity_proto_rawDesc = "" + + "\n" + + "\x12ha/v1/entity.proto\x12\x05ha.v1\x1a\x12ha/v1/common.proto\".\n" + + "\x0fGetStateRequest\x12\x1b\n" + + "\tentity_id\x18\x01 \x01(\tR\bentityId\"<\n" + + "\x10GetStateResponse\x12(\n" + + "\x05state\x18\x01 \x01(\v2\x12.ha.v1.EntityStateR\x05state\"J\n" + + "\x11ListStatesRequest\x12\x1d\n" + + "\n" + + "entity_ids\x18\x01 \x03(\tR\tentityIds\x12\x16\n" + + "\x06domain\x18\x02 \x01(\tR\x06domain\"@\n" + + "\x12ListStatesResponse\x12*\n" + + "\x06states\x18\x01 \x03(\v2\x12.ha.v1.EntityStateR\x06states2\x8f\x01\n" + + "\rEntityService\x12;\n" + + "\bGetState\x12\x16.ha.v1.GetStateRequest\x1a\x17.ha.v1.GetStateResponse\x12A\n" + + "\n" + + "ListStates\x12\x18.ha.v1.ListStatesRequest\x1a\x19.ha.v1.ListStatesResponseB4Z2gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1b\x06proto3" + +var ( + file_ha_v1_entity_proto_rawDescOnce sync.Once + file_ha_v1_entity_proto_rawDescData []byte +) + +func file_ha_v1_entity_proto_rawDescGZIP() []byte { + file_ha_v1_entity_proto_rawDescOnce.Do(func() { + file_ha_v1_entity_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ha_v1_entity_proto_rawDesc), len(file_ha_v1_entity_proto_rawDesc))) + }) + return file_ha_v1_entity_proto_rawDescData +} + +var file_ha_v1_entity_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_ha_v1_entity_proto_goTypes = []any{ + (*GetStateRequest)(nil), // 0: ha.v1.GetStateRequest + (*GetStateResponse)(nil), // 1: ha.v1.GetStateResponse + (*ListStatesRequest)(nil), // 2: ha.v1.ListStatesRequest + (*ListStatesResponse)(nil), // 3: ha.v1.ListStatesResponse + (*EntityState)(nil), // 4: ha.v1.EntityState +} +var file_ha_v1_entity_proto_depIdxs = []int32{ + 4, // 0: ha.v1.GetStateResponse.state:type_name -> ha.v1.EntityState + 4, // 1: ha.v1.ListStatesResponse.states:type_name -> ha.v1.EntityState + 0, // 2: ha.v1.EntityService.GetState:input_type -> ha.v1.GetStateRequest + 2, // 3: ha.v1.EntityService.ListStates:input_type -> ha.v1.ListStatesRequest + 1, // 4: ha.v1.EntityService.GetState:output_type -> ha.v1.GetStateResponse + 3, // 5: ha.v1.EntityService.ListStates:output_type -> ha.v1.ListStatesResponse + 4, // [4:6] is the sub-list for method output_type + 2, // [2:4] 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_entity_proto_init() } +func file_ha_v1_entity_proto_init() { + if File_ha_v1_entity_proto != nil { + return + } + file_ha_v1_common_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_entity_proto_rawDesc), len(file_ha_v1_entity_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ha_v1_entity_proto_goTypes, + DependencyIndexes: file_ha_v1_entity_proto_depIdxs, + MessageInfos: file_ha_v1_entity_proto_msgTypes, + }.Build() + File_ha_v1_entity_proto = out.File + file_ha_v1_entity_proto_goTypes = nil + file_ha_v1_entity_proto_depIdxs = nil +} diff --git a/gen/ha/v1/entity_grpc.pb.go b/gen/ha/v1/entity_grpc.pb.go new file mode 100644 index 0000000..01fa1b1 --- /dev/null +++ b/gen/ha/v1/entity_grpc.pb.go @@ -0,0 +1,159 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ha/v1/entity.proto + +package hav1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + EntityService_GetState_FullMethodName = "/ha.v1.EntityService/GetState" + EntityService_ListStates_FullMethodName = "/ha.v1.EntityService/ListStates" +) + +// EntityServiceClient is the client API for EntityService 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. +type EntityServiceClient interface { + GetState(ctx context.Context, in *GetStateRequest, opts ...grpc.CallOption) (*GetStateResponse, error) + ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error) +} + +type entityServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewEntityServiceClient(cc grpc.ClientConnInterface) EntityServiceClient { + return &entityServiceClient{cc} +} + +func (c *entityServiceClient) GetState(ctx context.Context, in *GetStateRequest, opts ...grpc.CallOption) (*GetStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetStateResponse) + err := c.cc.Invoke(ctx, EntityService_GetState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *entityServiceClient) ListStates(ctx context.Context, in *ListStatesRequest, opts ...grpc.CallOption) (*ListStatesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListStatesResponse) + err := c.cc.Invoke(ctx, EntityService_ListStates_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// EntityServiceServer is the server API for EntityService service. +// All implementations must embed UnimplementedEntityServiceServer +// for forward compatibility. +type EntityServiceServer interface { + GetState(context.Context, *GetStateRequest) (*GetStateResponse, error) + ListStates(context.Context, *ListStatesRequest) (*ListStatesResponse, error) + mustEmbedUnimplementedEntityServiceServer() +} + +// UnimplementedEntityServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedEntityServiceServer struct{} + +func (UnimplementedEntityServiceServer) GetState(context.Context, *GetStateRequest) (*GetStateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetState not implemented") +} +func (UnimplementedEntityServiceServer) ListStates(context.Context, *ListStatesRequest) (*ListStatesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListStates not implemented") +} +func (UnimplementedEntityServiceServer) mustEmbedUnimplementedEntityServiceServer() {} +func (UnimplementedEntityServiceServer) testEmbeddedByValue() {} + +// UnsafeEntityServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to EntityServiceServer will +// result in compilation errors. +type UnsafeEntityServiceServer interface { + mustEmbedUnimplementedEntityServiceServer() +} + +func RegisterEntityServiceServer(s grpc.ServiceRegistrar, srv EntityServiceServer) { + // If the following call panics, it indicates UnimplementedEntityServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&EntityService_ServiceDesc, srv) +} + +func _EntityService_GetState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EntityServiceServer).GetState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: EntityService_GetState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EntityServiceServer).GetState(ctx, req.(*GetStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _EntityService_ListStates_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListStatesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EntityServiceServer).ListStates(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: EntityService_ListStates_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EntityServiceServer).ListStates(ctx, req.(*ListStatesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// EntityService_ServiceDesc is the grpc.ServiceDesc for EntityService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var EntityService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ha.v1.EntityService", + HandlerType: (*EntityServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetState", + Handler: _EntityService_GetState_Handler, + }, + { + MethodName: "ListStates", + Handler: _EntityService_ListStates_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ha/v1/entity.proto", +} diff --git a/gen/ha/v1/event.pb.go b/gen/ha/v1/event.pb.go new file mode 100644 index 0000000..6099724 --- /dev/null +++ b/gen/ha/v1/event.pb.go @@ -0,0 +1,218 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ha/v1/event.proto + +package hav1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SubscribeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityIds []string `protobuf:"bytes,1,rep,name=entity_ids,json=entityIds,proto3" json:"entity_ids,omitempty"` + Domains []string `protobuf:"bytes,2,rep,name=domains,proto3" json:"domains,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscribeRequest) Reset() { + *x = SubscribeRequest{} + mi := &file_ha_v1_event_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscribeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscribeRequest) ProtoMessage() {} + +func (x *SubscribeRequest) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_event_proto_msgTypes[0] + 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 SubscribeRequest.ProtoReflect.Descriptor instead. +func (*SubscribeRequest) Descriptor() ([]byte, []int) { + return file_ha_v1_event_proto_rawDescGZIP(), []int{0} +} + +func (x *SubscribeRequest) GetEntityIds() []string { + if x != nil { + return x.EntityIds + } + return nil +} + +func (x *SubscribeRequest) GetDomains() []string { + if x != nil { + return x.Domains + } + return nil +} + +type StateChangeEvent struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + OldState *EntityState `protobuf:"bytes,2,opt,name=old_state,json=oldState,proto3,oneof" json:"old_state,omitempty"` // absent on first appearance + NewState *EntityState `protobuf:"bytes,3,opt,name=new_state,json=newState,proto3" json:"new_state,omitempty"` + EventTime string `protobuf:"bytes,4,opt,name=event_time,json=eventTime,proto3" json:"event_time,omitempty"` // RFC3339 + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StateChangeEvent) Reset() { + *x = StateChangeEvent{} + mi := &file_ha_v1_event_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StateChangeEvent) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StateChangeEvent) ProtoMessage() {} + +func (x *StateChangeEvent) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_event_proto_msgTypes[1] + 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 StateChangeEvent.ProtoReflect.Descriptor instead. +func (*StateChangeEvent) Descriptor() ([]byte, []int) { + return file_ha_v1_event_proto_rawDescGZIP(), []int{1} +} + +func (x *StateChangeEvent) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +func (x *StateChangeEvent) GetOldState() *EntityState { + if x != nil { + return x.OldState + } + return nil +} + +func (x *StateChangeEvent) GetNewState() *EntityState { + if x != nil { + return x.NewState + } + return nil +} + +func (x *StateChangeEvent) GetEventTime() string { + if x != nil { + return x.EventTime + } + return "" +} + +var File_ha_v1_event_proto protoreflect.FileDescriptor + +const file_ha_v1_event_proto_rawDesc = "" + + "\n" + + "\x11ha/v1/event.proto\x12\x05ha.v1\x1a\x12ha/v1/common.proto\"K\n" + + "\x10SubscribeRequest\x12\x1d\n" + + "\n" + + "entity_ids\x18\x01 \x03(\tR\tentityIds\x12\x18\n" + + "\adomains\x18\x02 \x03(\tR\adomains\"\xc3\x01\n" + + "\x10StateChangeEvent\x12\x1b\n" + + "\tentity_id\x18\x01 \x01(\tR\bentityId\x124\n" + + "\told_state\x18\x02 \x01(\v2\x12.ha.v1.EntityStateH\x00R\boldState\x88\x01\x01\x12/\n" + + "\tnew_state\x18\x03 \x01(\v2\x12.ha.v1.EntityStateR\bnewState\x12\x1d\n" + + "\n" + + "event_time\x18\x04 \x01(\tR\teventTimeB\f\n" + + "\n" + + "_old_state2O\n" + + "\fEventService\x12?\n" + + "\tSubscribe\x12\x17.ha.v1.SubscribeRequest\x1a\x17.ha.v1.StateChangeEvent0\x01B4Z2gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1b\x06proto3" + +var ( + file_ha_v1_event_proto_rawDescOnce sync.Once + file_ha_v1_event_proto_rawDescData []byte +) + +func file_ha_v1_event_proto_rawDescGZIP() []byte { + file_ha_v1_event_proto_rawDescOnce.Do(func() { + file_ha_v1_event_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ha_v1_event_proto_rawDesc), len(file_ha_v1_event_proto_rawDesc))) + }) + return file_ha_v1_event_proto_rawDescData +} + +var file_ha_v1_event_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_ha_v1_event_proto_goTypes = []any{ + (*SubscribeRequest)(nil), // 0: ha.v1.SubscribeRequest + (*StateChangeEvent)(nil), // 1: ha.v1.StateChangeEvent + (*EntityState)(nil), // 2: ha.v1.EntityState +} +var file_ha_v1_event_proto_depIdxs = []int32{ + 2, // 0: ha.v1.StateChangeEvent.old_state:type_name -> ha.v1.EntityState + 2, // 1: ha.v1.StateChangeEvent.new_state:type_name -> ha.v1.EntityState + 0, // 2: ha.v1.EventService.Subscribe:input_type -> ha.v1.SubscribeRequest + 1, // 3: ha.v1.EventService.Subscribe:output_type -> ha.v1.StateChangeEvent + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] 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_event_proto_init() } +func file_ha_v1_event_proto_init() { + if File_ha_v1_event_proto != nil { + return + } + file_ha_v1_common_proto_init() + file_ha_v1_event_proto_msgTypes[1].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_ha_v1_event_proto_rawDesc), len(file_ha_v1_event_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ha_v1_event_proto_goTypes, + DependencyIndexes: file_ha_v1_event_proto_depIdxs, + MessageInfos: file_ha_v1_event_proto_msgTypes, + }.Build() + File_ha_v1_event_proto = out.File + file_ha_v1_event_proto_goTypes = nil + file_ha_v1_event_proto_depIdxs = nil +} diff --git a/gen/ha/v1/event_grpc.pb.go b/gen/ha/v1/event_grpc.pb.go new file mode 100644 index 0000000..50fa593 --- /dev/null +++ b/gen/ha/v1/event_grpc.pb.go @@ -0,0 +1,144 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ha/v1/event.proto + +package hav1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + EventService_Subscribe_FullMethodName = "/ha.v1.EventService/Subscribe" +) + +// EventServiceClient is the client API for EventService 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 EventService fan-out. +// Architecture: +// 1. adapters/secondary/ha/websocket.go connects to HA WebSocket, +// authenticates, and subscribes to state_changed events. +// 2. An internal broker (internal/fanout/broker.go) holds a sync.Map of +// subscriber channels, one per active Subscribe stream. +// 3. The WebSocket adapter publishes to the broker; the gRPC Subscribe +// handler reads from its channel and streams to the client. +// 4. On client disconnect (ctx.Done()), the handler deregisters its channel. +type EventServiceClient interface { + Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StateChangeEvent], error) +} + +type eventServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewEventServiceClient(cc grpc.ClientConnInterface) EventServiceClient { + return &eventServiceClient{cc} +} + +func (c *eventServiceClient) Subscribe(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[StateChangeEvent], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &EventService_ServiceDesc.Streams[0], EventService_Subscribe_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SubscribeRequest, StateChangeEvent]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type EventService_SubscribeClient = grpc.ServerStreamingClient[StateChangeEvent] + +// EventServiceServer is the server API for EventService service. +// All implementations must embed UnimplementedEventServiceServer +// for forward compatibility. +// +// TODO: implement EventService fan-out. +// Architecture: +// 1. adapters/secondary/ha/websocket.go connects to HA WebSocket, +// authenticates, and subscribes to state_changed events. +// 2. An internal broker (internal/fanout/broker.go) holds a sync.Map of +// subscriber channels, one per active Subscribe stream. +// 3. The WebSocket adapter publishes to the broker; the gRPC Subscribe +// handler reads from its channel and streams to the client. +// 4. On client disconnect (ctx.Done()), the handler deregisters its channel. +type EventServiceServer interface { + Subscribe(*SubscribeRequest, grpc.ServerStreamingServer[StateChangeEvent]) error + mustEmbedUnimplementedEventServiceServer() +} + +// UnimplementedEventServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedEventServiceServer struct{} + +func (UnimplementedEventServiceServer) Subscribe(*SubscribeRequest, grpc.ServerStreamingServer[StateChangeEvent]) error { + return status.Error(codes.Unimplemented, "method Subscribe not implemented") +} +func (UnimplementedEventServiceServer) mustEmbedUnimplementedEventServiceServer() {} +func (UnimplementedEventServiceServer) testEmbeddedByValue() {} + +// UnsafeEventServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to EventServiceServer will +// result in compilation errors. +type UnsafeEventServiceServer interface { + mustEmbedUnimplementedEventServiceServer() +} + +func RegisterEventServiceServer(s grpc.ServiceRegistrar, srv EventServiceServer) { + // If the following call panics, it indicates UnimplementedEventServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&EventService_ServiceDesc, srv) +} + +func _EventService_Subscribe_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SubscribeRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(EventServiceServer).Subscribe(m, &grpc.GenericServerStream[SubscribeRequest, StateChangeEvent]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type EventService_SubscribeServer = grpc.ServerStreamingServer[StateChangeEvent] + +// EventService_ServiceDesc is the grpc.ServiceDesc for EventService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var EventService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ha.v1.EventService", + HandlerType: (*EventServiceServer)(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "Subscribe", + Handler: _EventService_Subscribe_Handler, + ServerStreams: true, + }, + }, + Metadata: "ha/v1/event.proto", +} diff --git a/gen/ha/v1/light.pb.go b/gen/ha/v1/light.pb.go new file mode 100644 index 0000000..32eea95 --- /dev/null +++ b/gen/ha/v1/light.pb.go @@ -0,0 +1,338 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ha/v1/light.proto + +package hav1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// optional fields require protobuf 3.15+ / buf >= 1.0. They generate +// pointer fields in Go with Has*() accessor methods. This is intentional — +// it lets the gateway distinguish "brightness not set" from "brightness = 0". +type TurnOnRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + BrightnessPct *uint32 `protobuf:"varint,2,opt,name=brightness_pct,json=brightnessPct,proto3,oneof" json:"brightness_pct,omitempty"` // 0–100 + ColorTempKelvin *uint32 `protobuf:"varint,3,opt,name=color_temp_kelvin,json=colorTempKelvin,proto3,oneof" json:"color_temp_kelvin,omitempty"` // e.g. 2700–6500 + RgbColor *RGBColor `protobuf:"bytes,4,opt,name=rgb_color,json=rgbColor,proto3,oneof" json:"rgb_color,omitempty"` // ignored if color_temp_kelvin set + Transition *uint32 `protobuf:"varint,5,opt,name=transition,proto3,oneof" json:"transition,omitempty"` // seconds + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TurnOnRequest) Reset() { + *x = TurnOnRequest{} + mi := &file_ha_v1_light_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TurnOnRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TurnOnRequest) ProtoMessage() {} + +func (x *TurnOnRequest) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_light_proto_msgTypes[0] + 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 TurnOnRequest.ProtoReflect.Descriptor instead. +func (*TurnOnRequest) Descriptor() ([]byte, []int) { + return file_ha_v1_light_proto_rawDescGZIP(), []int{0} +} + +func (x *TurnOnRequest) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +func (x *TurnOnRequest) GetBrightnessPct() uint32 { + if x != nil && x.BrightnessPct != nil { + return *x.BrightnessPct + } + return 0 +} + +func (x *TurnOnRequest) GetColorTempKelvin() uint32 { + if x != nil && x.ColorTempKelvin != nil { + return *x.ColorTempKelvin + } + return 0 +} + +func (x *TurnOnRequest) GetRgbColor() *RGBColor { + if x != nil { + return x.RgbColor + } + return nil +} + +func (x *TurnOnRequest) GetTransition() uint32 { + if x != nil && x.Transition != nil { + return *x.Transition + } + return 0 +} + +type TurnOffRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + Transition *uint32 `protobuf:"varint,2,opt,name=transition,proto3,oneof" json:"transition,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TurnOffRequest) Reset() { + *x = TurnOffRequest{} + mi := &file_ha_v1_light_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TurnOffRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TurnOffRequest) ProtoMessage() {} + +func (x *TurnOffRequest) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_light_proto_msgTypes[1] + 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 TurnOffRequest.ProtoReflect.Descriptor instead. +func (*TurnOffRequest) Descriptor() ([]byte, []int) { + return file_ha_v1_light_proto_rawDescGZIP(), []int{1} +} + +func (x *TurnOffRequest) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +func (x *TurnOffRequest) GetTransition() uint32 { + if x != nil && x.Transition != nil { + return *x.Transition + } + return 0 +} + +type ToggleRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ToggleRequest) Reset() { + *x = ToggleRequest{} + mi := &file_ha_v1_light_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ToggleRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ToggleRequest) ProtoMessage() {} + +func (x *ToggleRequest) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_light_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 ToggleRequest.ProtoReflect.Descriptor instead. +func (*ToggleRequest) Descriptor() ([]byte, []int) { + return file_ha_v1_light_proto_rawDescGZIP(), []int{2} +} + +func (x *ToggleRequest) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +type LightResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State *EntityState `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LightResponse) Reset() { + *x = LightResponse{} + mi := &file_ha_v1_light_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LightResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LightResponse) ProtoMessage() {} + +func (x *LightResponse) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_light_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 LightResponse.ProtoReflect.Descriptor instead. +func (*LightResponse) Descriptor() ([]byte, []int) { + return file_ha_v1_light_proto_rawDescGZIP(), []int{3} +} + +func (x *LightResponse) GetState() *EntityState { + if x != nil { + return x.State + } + return nil +} + +var File_ha_v1_light_proto protoreflect.FileDescriptor + +const file_ha_v1_light_proto_rawDesc = "" + + "\n" + + "\x11ha/v1/light.proto\x12\x05ha.v1\x1a\x12ha/v1/common.proto\"\xa7\x02\n" + + "\rTurnOnRequest\x12\x1b\n" + + "\tentity_id\x18\x01 \x01(\tR\bentityId\x12*\n" + + "\x0ebrightness_pct\x18\x02 \x01(\rH\x00R\rbrightnessPct\x88\x01\x01\x12/\n" + + "\x11color_temp_kelvin\x18\x03 \x01(\rH\x01R\x0fcolorTempKelvin\x88\x01\x01\x121\n" + + "\trgb_color\x18\x04 \x01(\v2\x0f.ha.v1.RGBColorH\x02R\brgbColor\x88\x01\x01\x12#\n" + + "\n" + + "transition\x18\x05 \x01(\rH\x03R\n" + + "transition\x88\x01\x01B\x11\n" + + "\x0f_brightness_pctB\x14\n" + + "\x12_color_temp_kelvinB\f\n" + + "\n" + + "_rgb_colorB\r\n" + + "\v_transition\"a\n" + + "\x0eTurnOffRequest\x12\x1b\n" + + "\tentity_id\x18\x01 \x01(\tR\bentityId\x12#\n" + + "\n" + + "transition\x18\x02 \x01(\rH\x00R\n" + + "transition\x88\x01\x01B\r\n" + + "\v_transition\",\n" + + "\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" + + "\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" + +var ( + file_ha_v1_light_proto_rawDescOnce sync.Once + file_ha_v1_light_proto_rawDescData []byte +) + +func file_ha_v1_light_proto_rawDescGZIP() []byte { + file_ha_v1_light_proto_rawDescOnce.Do(func() { + file_ha_v1_light_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ha_v1_light_proto_rawDesc), len(file_ha_v1_light_proto_rawDesc))) + }) + return file_ha_v1_light_proto_rawDescData +} + +var file_ha_v1_light_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +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 +} +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 +} + +func init() { file_ha_v1_light_proto_init() } +func file_ha_v1_light_proto_init() { + if File_ha_v1_light_proto != nil { + return + } + file_ha_v1_common_proto_init() + file_ha_v1_light_proto_msgTypes[0].OneofWrappers = []any{} + file_ha_v1_light_proto_msgTypes[1].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + 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, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ha_v1_light_proto_goTypes, + DependencyIndexes: file_ha_v1_light_proto_depIdxs, + MessageInfos: file_ha_v1_light_proto_msgTypes, + }.Build() + File_ha_v1_light_proto = out.File + file_ha_v1_light_proto_goTypes = nil + file_ha_v1_light_proto_depIdxs = nil +} diff --git a/gen/ha/v1/light_grpc.pb.go b/gen/ha/v1/light_grpc.pb.go new file mode 100644 index 0000000..7ae446d --- /dev/null +++ b/gen/ha/v1/light_grpc.pb.go @@ -0,0 +1,197 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ha/v1/light.proto + +package hav1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +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" +) + +// LightServiceClient is the client API for LightService 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. +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) +} + +type lightServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewLightServiceClient(cc grpc.ClientConnInterface) LightServiceClient { + return &lightServiceClient{cc} +} + +func (c *lightServiceClient) TurnOn(ctx context.Context, in *TurnOnRequest, opts ...grpc.CallOption) (*LightResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LightResponse) + err := c.cc.Invoke(ctx, LightService_TurnOn_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *lightServiceClient) TurnOff(ctx context.Context, in *TurnOffRequest, opts ...grpc.CallOption) (*LightResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LightResponse) + err := c.cc.Invoke(ctx, LightService_TurnOff_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *lightServiceClient) Toggle(ctx context.Context, in *ToggleRequest, opts ...grpc.CallOption) (*LightResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LightResponse) + err := c.cc.Invoke(ctx, LightService_Toggle_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. +type LightServiceServer interface { + TurnOn(context.Context, *TurnOnRequest) (*LightResponse, error) + TurnOff(context.Context, *TurnOffRequest) (*LightResponse, error) + Toggle(context.Context, *ToggleRequest) (*LightResponse, error) + mustEmbedUnimplementedLightServiceServer() +} + +// UnimplementedLightServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedLightServiceServer struct{} + +func (UnimplementedLightServiceServer) TurnOn(context.Context, *TurnOnRequest) (*LightResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TurnOn not implemented") +} +func (UnimplementedLightServiceServer) TurnOff(context.Context, *TurnOffRequest) (*LightResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TurnOff not implemented") +} +func (UnimplementedLightServiceServer) Toggle(context.Context, *ToggleRequest) (*LightResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Toggle not implemented") +} +func (UnimplementedLightServiceServer) mustEmbedUnimplementedLightServiceServer() {} +func (UnimplementedLightServiceServer) testEmbeddedByValue() {} + +// UnsafeLightServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to LightServiceServer will +// result in compilation errors. +type UnsafeLightServiceServer interface { + mustEmbedUnimplementedLightServiceServer() +} + +func RegisterLightServiceServer(s grpc.ServiceRegistrar, srv LightServiceServer) { + // If the following call panics, it indicates UnimplementedLightServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&LightService_ServiceDesc, srv) +} + +func _LightService_TurnOn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TurnOnRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightServiceServer).TurnOn(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: LightService_TurnOn_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightServiceServer).TurnOn(ctx, req.(*TurnOnRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LightService_TurnOff_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TurnOffRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightServiceServer).TurnOff(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: LightService_TurnOff_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightServiceServer).TurnOff(ctx, req.(*TurnOffRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _LightService_Toggle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ToggleRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(LightServiceServer).Toggle(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: LightService_Toggle_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(LightServiceServer).Toggle(ctx, req.(*ToggleRequest)) + } + 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) +var LightService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ha.v1.LightService", + HandlerType: (*LightServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "TurnOn", + Handler: _LightService_TurnOn_Handler, + }, + { + MethodName: "TurnOff", + Handler: _LightService_TurnOff_Handler, + }, + { + MethodName: "Toggle", + Handler: _LightService_Toggle_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 new file mode 100644 index 0000000..ec9c635 --- /dev/null +++ b/gen/ha/v1/switch.pb.go @@ -0,0 +1,182 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: ha/v1/switch.proto + +package hav1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type SwitchRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + EntityId string `protobuf:"bytes,1,opt,name=entity_id,json=entityId,proto3" json:"entity_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SwitchRequest) Reset() { + *x = SwitchRequest{} + mi := &file_ha_v1_switch_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SwitchRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SwitchRequest) ProtoMessage() {} + +func (x *SwitchRequest) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_switch_proto_msgTypes[0] + 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 SwitchRequest.ProtoReflect.Descriptor instead. +func (*SwitchRequest) Descriptor() ([]byte, []int) { + return file_ha_v1_switch_proto_rawDescGZIP(), []int{0} +} + +func (x *SwitchRequest) GetEntityId() string { + if x != nil { + return x.EntityId + } + return "" +} + +type SwitchResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State *EntityState `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SwitchResponse) Reset() { + *x = SwitchResponse{} + mi := &file_ha_v1_switch_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SwitchResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SwitchResponse) ProtoMessage() {} + +func (x *SwitchResponse) ProtoReflect() protoreflect.Message { + mi := &file_ha_v1_switch_proto_msgTypes[1] + 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 SwitchResponse.ProtoReflect.Descriptor instead. +func (*SwitchResponse) Descriptor() ([]byte, []int) { + return file_ha_v1_switch_proto_rawDescGZIP(), []int{1} +} + +func (x *SwitchResponse) GetState() *EntityState { + if x != nil { + return x.State + } + return nil +} + +var File_ha_v1_switch_proto protoreflect.FileDescriptor + +const file_ha_v1_switch_proto_rawDesc = "" + + "\n" + + "\x12ha/v1/switch.proto\x12\x05ha.v1\x1a\x12ha/v1/common.proto\",\n" + + "\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" + + "\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" + +var ( + file_ha_v1_switch_proto_rawDescOnce sync.Once + file_ha_v1_switch_proto_rawDescData []byte +) + +func file_ha_v1_switch_proto_rawDescGZIP() []byte { + file_ha_v1_switch_proto_rawDescOnce.Do(func() { + file_ha_v1_switch_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_ha_v1_switch_proto_rawDesc), len(file_ha_v1_switch_proto_rawDesc))) + }) + return file_ha_v1_switch_proto_rawDescData +} + +var file_ha_v1_switch_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +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 +} +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 +} + +func init() { file_ha_v1_switch_proto_init() } +func file_ha_v1_switch_proto_init() { + if File_ha_v1_switch_proto != nil { + return + } + file_ha_v1_common_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + 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, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_ha_v1_switch_proto_goTypes, + DependencyIndexes: file_ha_v1_switch_proto_depIdxs, + MessageInfos: file_ha_v1_switch_proto_msgTypes, + }.Build() + File_ha_v1_switch_proto = out.File + file_ha_v1_switch_proto_goTypes = nil + file_ha_v1_switch_proto_depIdxs = nil +} diff --git a/gen/ha/v1/switch_grpc.pb.go b/gen/ha/v1/switch_grpc.pb.go new file mode 100644 index 0000000..806bc0e --- /dev/null +++ b/gen/ha/v1/switch_grpc.pb.go @@ -0,0 +1,209 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.1 +// - protoc (unknown) +// source: ha/v1/switch.proto + +package hav1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +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" +) + +// 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) +} + +type switchServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewSwitchServiceClient(cc grpc.ClientConnInterface) SwitchServiceClient { + return &switchServiceClient{cc} +} + +func (c *switchServiceClient) TurnOn(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SwitchResponse) + err := c.cc.Invoke(ctx, SwitchService_TurnOn_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *switchServiceClient) TurnOff(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SwitchResponse) + err := c.cc.Invoke(ctx, SwitchService_TurnOff_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *switchServiceClient) Toggle(ctx context.Context, in *SwitchRequest, opts ...grpc.CallOption) (*SwitchResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SwitchResponse) + err := c.cc.Invoke(ctx, SwitchService_Toggle_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) + mustEmbedUnimplementedSwitchServiceServer() +} + +// UnimplementedSwitchServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedSwitchServiceServer struct{} + +func (UnimplementedSwitchServiceServer) TurnOn(context.Context, *SwitchRequest) (*SwitchResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TurnOn not implemented") +} +func (UnimplementedSwitchServiceServer) TurnOff(context.Context, *SwitchRequest) (*SwitchResponse, error) { + return nil, status.Error(codes.Unimplemented, "method TurnOff not implemented") +} +func (UnimplementedSwitchServiceServer) Toggle(context.Context, *SwitchRequest) (*SwitchResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Toggle not implemented") +} +func (UnimplementedSwitchServiceServer) mustEmbedUnimplementedSwitchServiceServer() {} +func (UnimplementedSwitchServiceServer) testEmbeddedByValue() {} + +// UnsafeSwitchServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to SwitchServiceServer will +// result in compilation errors. +type UnsafeSwitchServiceServer interface { + mustEmbedUnimplementedSwitchServiceServer() +} + +func RegisterSwitchServiceServer(s grpc.ServiceRegistrar, srv SwitchServiceServer) { + // If the following call panics, it indicates UnimplementedSwitchServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&SwitchService_ServiceDesc, srv) +} + +func _SwitchService_TurnOn_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SwitchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwitchServiceServer).TurnOn(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SwitchService_TurnOn_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwitchServiceServer).TurnOn(ctx, req.(*SwitchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwitchService_TurnOff_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SwitchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwitchServiceServer).TurnOff(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SwitchService_TurnOff_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwitchServiceServer).TurnOff(ctx, req.(*SwitchRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _SwitchService_Toggle_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SwitchRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(SwitchServiceServer).Toggle(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: SwitchService_Toggle_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(SwitchServiceServer).Toggle(ctx, req.(*SwitchRequest)) + } + 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) +var SwitchService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "ha.v1.SwitchService", + HandlerType: (*SwitchServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "TurnOn", + Handler: _SwitchService_TurnOn_Handler, + }, + { + MethodName: "TurnOff", + Handler: _SwitchService_TurnOff_Handler, + }, + { + MethodName: "Toggle", + Handler: _SwitchService_Toggle_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "ha/v1/switch.proto", +} diff --git a/go.work b/go.work new file mode 100644 index 0000000..721af23 --- /dev/null +++ b/go.work @@ -0,0 +1,6 @@ +go 1.26 + +use ( + ./gen + ./ha-gateway +) diff --git a/go.work.sum b/go.work.sum new file mode 100644 index 0000000..439f9d5 --- /dev/null +++ b/go.work.sum @@ -0,0 +1,92 @@ +cel.dev/expr v0.19.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= +cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +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/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= +github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= +github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +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/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/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= +go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= +go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= +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/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= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +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/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +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= +google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +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= diff --git a/ha-gateway/.env.example b/ha-gateway/.env.example new file mode 100644 index 0000000..f563a2f --- /dev/null +++ b/ha-gateway/.env.example @@ -0,0 +1,4 @@ +GRPC_PORT=50051 +HA_BASE_URL=http://ha.home.arpa:8123 +HA_TOKEN=your-long-lived-token-here +OTEL_ENDPOINT= diff --git a/ha-gateway/Dockerfile b/ha-gateway/Dockerfile new file mode 100644 index 0000000..8c97c31 --- /dev/null +++ b/ha-gateway/Dockerfile @@ -0,0 +1,19 @@ +FROM golang:1.26-alpine AS builder +WORKDIR /workspace + +COPY go.work go.work.sum ./ +COPY gen/ ./gen/ +COPY ha-gateway/ ./ha-gateway/ + +WORKDIR /workspace/ha-gateway +RUN go mod download + +ARG VERSION=dev +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags="-s -w -X main.version=${VERSION}" \ + -o /ha-gateway ./cmd/gateway + +FROM gcr.io/distroless/static:nonroot +COPY --from=builder /ha-gateway /ha-gateway +EXPOSE 50051 +ENTRYPOINT ["/ha-gateway"] diff --git a/ha-gateway/cmd/gateway/main.go b/ha-gateway/cmd/gateway/main.go new file mode 100644 index 0000000..0ae9ab7 --- /dev/null +++ b/ha-gateway/cmd/gateway/main.go @@ -0,0 +1,143 @@ +package main + +import ( + "context" + "log/slog" + "net" + "os" + "os/signal" + "syscall" + "time" + + "github.com/joho/godotenv" + "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/app" + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/config" + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/telemetry" +) + +// MEMO: auth is not implemented. +// Add one of the following before exposing this service to any untrusted network: +// Option A — shared API key per client: unary + stream interceptors read +// "authorization" from gRPC metadata and compare to a secret +// from config. Good for small number of known clients. +// Option B — mTLS (recommended): tls.Config with ClientAuth: RequireAndVerifyClientCert, +// cert pool from the internal CA. Each client gets a cert from +// cert-manager. No runtime auth dependency, identity in the cert CN/SAN. + +// version is set at build time via -ldflags "-X main.version=". +var version = "dev" + +func main() { + // 1. Load .env if present (ignored in K8s where env vars come from Secrets/ConfigMaps). + _ = godotenv.Load() + + // 2. Load and validate config. + cfg, err := config.Load() + if err != nil { + slog.Error("config error", "err", err) + os.Exit(1) + } + + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) + defer stop() + + // 3. Set up telemetry. + shutdown, err := telemetry.Setup(ctx, cfg, version) + if err != nil { + slog.Error("telemetry setup failed", "err", err) + os.Exit(1) + } + + // 4. Build the secondary adapter. + haClient := ha.NewClient(cfg) + + // 5. Build application services. + entityApp := app.NewEntityApp(haClient) + lightApp := app.NewLightApp(haClient) + + // 6. Build the gRPC server with OTel stats handler and logging interceptors. + srv := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), + grpc.ChainUnaryInterceptor(loggingUnaryInterceptor), + grpc.ChainStreamInterceptor(loggingStreamInterceptor), + ) + + // 7. Register services. + hav1.RegisterEntityServiceServer(srv, grpcadapter.NewEntityGRPC(entityApp)) + hav1.RegisterLightServiceServer(srv, grpcadapter.NewLightGRPC(lightApp)) + hav1.RegisterSwitchServiceServer(srv, &grpcadapter.SwitchGRPC{}) + hav1.RegisterEventServiceServer(srv, &grpcadapter.EventGRPC{}) + + // 8. Start listener. + lis, err := net.Listen("tcp", ":"+cfg.GRPCPort) + if err != nil { + slog.Error("listen failed", "err", err) + os.Exit(1) + } + + // 9. Serve in background. + go func() { + slog.Info("listening", "port", cfg.GRPCPort, "version", version) + if err := srv.Serve(lis); err != nil { + slog.Error("serve failed", "err", err) + } + }() + + // 10. Block until signal. + <-ctx.Done() + slog.Info("shutting down") + + // 11. Graceful stop, then flush telemetry. + srv.GracefulStop() + + shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + if err := shutdown(shutdownCtx); err != nil { + slog.Error("telemetry shutdown error", "err", err) + } +} + +func loggingUnaryInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + start := time.Now() + resp, err := handler(ctx, req) + attrs := []any{ + "method", info.FullMethod, + "duration", time.Since(start).String(), + } + if p, ok := peer.FromContext(ctx); ok { + attrs = append(attrs, "peer", p.Addr.String()) + } + if err != nil { + attrs = append(attrs, "err", err) + slog.ErrorContext(ctx, "rpc", attrs...) + } else { + slog.InfoContext(ctx, "rpc", attrs...) + } + return resp, err +} + +func loggingStreamInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + start := time.Now() + err := handler(srv, ss) + attrs := []any{ + "method", info.FullMethod, + "duration", time.Since(start).String(), + } + if p, ok := peer.FromContext(ss.Context()); ok { + attrs = append(attrs, "peer", p.Addr.String()) + } + if err != nil { + attrs = append(attrs, "err", err) + slog.ErrorContext(ss.Context(), "rpc stream", attrs...) + } else { + slog.InfoContext(ss.Context(), "rpc stream", attrs...) + } + return err +} diff --git a/ha-gateway/go.mod b/ha-gateway/go.mod new file mode 100644 index 0000000..08a3148 --- /dev/null +++ b/ha-gateway/go.mod @@ -0,0 +1,37 @@ +module gitea.nik4nao.com/nik/home-services/ha-gateway + +go 1.26 + +require ( + gitea.nik4nao.com/nik/home-services/gen v0.0.0 + github.com/joho/godotenv v1.5.1 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 + go.opentelemetry.io/otel v1.39.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 + go.opentelemetry.io/otel/sdk v1.39.0 + go.opentelemetry.io/otel/sdk/metric v1.39.0 + google.golang.org/grpc v1.79.3 +) + +require ( + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.5.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) + +replace gitea.nik4nao.com/nik/home-services/gen => ../gen diff --git a/ha-gateway/go.sum b/ha-gateway/go.sum new file mode 100644 index 0000000..ee382c2 --- /dev/null +++ b/ha-gateway/go.sum @@ -0,0 +1,67 @@ +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +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.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +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.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +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.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +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-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= +google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/ha-gateway/internal/adapters/primary/grpc/entity.go b/ha-gateway/internal/adapters/primary/grpc/entity.go new file mode 100644 index 0000000..20c45fc --- /dev/null +++ b/ha-gateway/internal/adapters/primary/grpc/entity.go @@ -0,0 +1,59 @@ +package grpc + +import ( + "context" + "errors" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain" + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driving" +) + +type EntityGRPC struct { + hav1.UnimplementedEntityServiceServer + svc driving.EntityService +} + +func NewEntityGRPC(svc driving.EntityService) *EntityGRPC { + return &EntityGRPC{svc: svc} +} + +func (h *EntityGRPC) GetState(ctx context.Context, req *hav1.GetStateRequest) (*hav1.GetStateResponse, error) { + s, err := h.svc.GetState(ctx, domain.EntityID(req.EntityId)) + if err != nil { + return nil, grpcError(err) + } + return &hav1.GetStateResponse{State: domainStateToProto(s)}, nil +} + +func (h *EntityGRPC) ListStates(ctx context.Context, req *hav1.ListStatesRequest) (*hav1.ListStatesResponse, error) { + ids := make([]domain.EntityID, len(req.EntityIds)) + for i, id := range req.EntityIds { + ids[i] = domain.EntityID(id) + } + + states, err := h.svc.ListStates(ctx, ids, req.Domain) + if err != nil { + return nil, grpcError(err) + } + + proto := make([]*hav1.EntityState, len(states)) + for i, s := range states { + proto[i] = domainStateToProto(s) + } + return &hav1.ListStatesResponse{States: proto}, nil +} + +// grpcError maps domain errors to appropriate gRPC status codes. +func grpcError(err error) error { + if errors.Is(err, ErrNotFound) { + return status.Errorf(codes.NotFound, "%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") diff --git a/ha-gateway/internal/adapters/primary/grpc/event.go b/ha-gateway/internal/adapters/primary/grpc/event.go new file mode 100644 index 0000000..26bcac4 --- /dev/null +++ b/ha-gateway/internal/adapters/primary/grpc/event.go @@ -0,0 +1,12 @@ +package grpc + +import ( + hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" +) + +type EventGRPC struct { + hav1.UnimplementedEventServiceServer +} + +// Subscribe returns codes.Unimplemented via the embedded UnimplementedEventServiceServer. +// TODO: inject fanout broker here once websocket.go is implemented. diff --git a/ha-gateway/internal/adapters/primary/grpc/light.go b/ha-gateway/internal/adapters/primary/grpc/light.go new file mode 100644 index 0000000..be96385 --- /dev/null +++ b/ha-gateway/internal/adapters/primary/grpc/light.go @@ -0,0 +1,42 @@ +package grpc + +import ( + "context" + + hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain" + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driving" +) + +type LightGRPC struct { + hav1.UnimplementedLightServiceServer + svc driving.LightService +} + +func NewLightGRPC(svc driving.LightService) *LightGRPC { + return &LightGRPC{svc: svc} +} + +func (h *LightGRPC) TurnOn(ctx context.Context, req *hav1.TurnOnRequest) (*hav1.LightResponse, error) { + s, err := h.svc.TurnOn(ctx, protoTurnOnToParams(req)) + if err != nil { + return nil, grpcError(err) + } + return &hav1.LightResponse{State: domainStateToProto(s)}, nil +} + +func (h *LightGRPC) TurnOff(ctx context.Context, req *hav1.TurnOffRequest) (*hav1.LightResponse, error) { + s, err := h.svc.TurnOff(ctx, protoTurnOffToParams(req)) + if err != nil { + return nil, grpcError(err) + } + return &hav1.LightResponse{State: domainStateToProto(s)}, nil +} + +func (h *LightGRPC) Toggle(ctx context.Context, req *hav1.ToggleRequest) (*hav1.LightResponse, error) { + s, err := h.svc.Toggle(ctx, domain.EntityID(req.EntityId)) + if err != nil { + return nil, grpcError(err) + } + return &hav1.LightResponse{State: domainStateToProto(s)}, nil +} diff --git a/ha-gateway/internal/adapters/primary/grpc/mapping.go b/ha-gateway/internal/adapters/primary/grpc/mapping.go new file mode 100644 index 0000000..8e7755e --- /dev/null +++ b/ha-gateway/internal/adapters/primary/grpc/mapping.go @@ -0,0 +1,53 @@ +package grpc + +import ( + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain" + hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" +) + +func domainStateToProto(s *domain.EntityState) *hav1.EntityState { + return &hav1.EntityState{ + EntityId: string(s.EntityID), + State: s.State, + Attributes: s.Attributes, + LastChanged: s.LastChanged.Format("2006-01-02T15:04:05Z07:00"), + LastUpdated: s.LastUpdated.Format("2006-01-02T15:04:05Z07:00"), + } +} + +func protoTurnOnToParams(r *hav1.TurnOnRequest) domain.TurnOnParams { + p := domain.TurnOnParams{ + EntityID: domain.EntityID(r.EntityId), + } + if r.BrightnessPct != nil { + v := r.GetBrightnessPct() + p.BrightnessPct = &v + } + if r.ColorTempKelvin != nil { + v := r.GetColorTempKelvin() + p.ColorTempKelvin = &v + } + if r.RgbColor != nil { + p.RGBColor = &domain.RGBColor{ + R: uint8(r.RgbColor.R), + G: uint8(r.RgbColor.G), + B: uint8(r.RgbColor.B), + } + } + if r.Transition != nil { + v := r.GetTransition() + p.Transition = &v + } + return p +} + +func protoTurnOffToParams(r *hav1.TurnOffRequest) domain.TurnOffParams { + p := domain.TurnOffParams{ + EntityID: domain.EntityID(r.EntityId), + } + if r.Transition != nil { + v := r.GetTransition() + p.Transition = &v + } + return p +} diff --git a/ha-gateway/internal/adapters/primary/grpc/switch.go b/ha-gateway/internal/adapters/primary/grpc/switch.go new file mode 100644 index 0000000..2b2e559 --- /dev/null +++ b/ha-gateway/internal/adapters/primary/grpc/switch.go @@ -0,0 +1,12 @@ +package grpc + +import ( + hav1 "gitea.nik4nao.com/nik/home-services/gen/ha/v1" +) + +type SwitchGRPC struct { + hav1.UnimplementedSwitchServiceServer +} + +// All methods return codes.Unimplemented via the embedded UnimplementedSwitchServiceServer. +// TODO: follow the same pattern as LightGRPC once app/switch.go is implemented. diff --git a/ha-gateway/internal/adapters/secondary/ha/client.go b/ha-gateway/internal/adapters/secondary/ha/client.go new file mode 100644 index 0000000..3b7da87 --- /dev/null +++ b/ha-gateway/internal/adapters/secondary/ha/client.go @@ -0,0 +1,183 @@ +package ha + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/config" + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driven" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" +) + +var tracer = otel.Tracer("ha-gateway/ha-client") + +type Client struct { + baseURL string + token string + httpClient *http.Client +} + +func NewClient(cfg *config.Config) *Client { + return &Client{ + baseURL: strings.TrimRight(cfg.HABaseURL, "/"), + token: cfg.HAToken, + httpClient: &http.Client{Timeout: 10 * time.Second}, + } +} + +func (c *Client) GetState(ctx context.Context, entityID string) (*driven.HAState, error) { + ctx, span := tracer.Start(ctx, "ha.GetState") + defer span.End() + span.SetAttributes(attribute.String("entity_id", entityID)) + + var raw haStateRaw + if err := c.get(ctx, "/api/states/"+entityID, &raw); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err + } + return raw.toDriven() +} + +func (c *Client) ListStates(ctx context.Context) ([]*driven.HAState, error) { + ctx, span := tracer.Start(ctx, "ha.ListStates") + defer span.End() + + var raw []haStateRaw + if err := c.get(ctx, "/api/states", &raw); err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err + } + + out := make([]*driven.HAState, 0, len(raw)) + for i := range raw { + s, err := raw[i].toDriven() + if err != nil { + return nil, err + } + out = append(out, s) + } + return out, nil +} + +func (c *Client) CallService(ctx context.Context, domain, service string, payload map[string]any) ([]*driven.HAState, error) { + ctx, span := tracer.Start(ctx, "ha.CallService") + defer span.End() + span.SetAttributes( + attribute.String("ha.domain", domain), + attribute.String("ha.service", service), + ) + + body, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("marshal payload: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, + c.baseURL+"/api/services/"+domain+"/"+service, + strings.NewReader(string(body))) + if err != nil { + return nil, fmt.Errorf("build request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.httpClient.Do(req) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, fmt.Errorf("call service %s/%s: %w", domain, service, err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + preview := string(respBody) + if len(preview) > 200 { + preview = preview[:200] + } + err := fmt.Errorf("HA returned %d: %s", resp.StatusCode, preview) + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return nil, err + } + + var raw []haStateRaw + if err := json.Unmarshal(respBody, &raw); err != nil { + // HA may return an empty body or non-array on some calls; treat as empty. + return nil, nil + } + + out := make([]*driven.HAState, 0, len(raw)) + for i := range raw { + s, err := raw[i].toDriven() + if err != nil { + return nil, err + } + out = append(out, s) + } + return out, nil +} + +func (c *Client) get(ctx context.Context, path string, dst any) error { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.baseURL+path, nil) + if err != nil { + return fmt.Errorf("build request: %w", err) + } + req.Header.Set("Authorization", "Bearer "+c.token) + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("GET %s: %w", path, err) + } + defer resp.Body.Close() + + body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + preview := string(body) + if len(preview) > 200 { + preview = preview[:200] + } + return fmt.Errorf("HA returned %d for GET %s: %s", resp.StatusCode, path, preview) + } + + if err := json.Unmarshal(body, dst); err != nil { + return fmt.Errorf("decode response for GET %s: %w", path, err) + } + return nil +} + +// haStateRaw is the raw JSON shape returned by the HA REST API. +type haStateRaw struct { + EntityID string `json:"entity_id"` + State string `json:"state"` + Attributes map[string]any `json:"attributes"` + LastChanged string `json:"last_changed"` + LastUpdated string `json:"last_updated"` +} + +func (r *haStateRaw) toDriven() (*driven.HAState, error) { + lc, err := time.Parse(time.RFC3339, r.LastChanged) + if err != nil { + lc = time.Time{} + } + lu, err := time.Parse(time.RFC3339, r.LastUpdated) + if err != nil { + lu = time.Time{} + } + return &driven.HAState{ + EntityID: r.EntityID, + State: r.State, + Attributes: r.Attributes, + LastChanged: lc, + LastUpdated: lu, + }, nil +} diff --git a/ha-gateway/internal/adapters/secondary/ha/websocket.go b/ha-gateway/internal/adapters/secondary/ha/websocket.go new file mode 100644 index 0000000..ed73575 --- /dev/null +++ b/ha-gateway/internal/adapters/secondary/ha/websocket.go @@ -0,0 +1,7 @@ +package ha + +// TODO: implement HA WebSocket client. +// Auth flow: receive auth_required → send {"type":"auth","access_token":"..."} → receive auth_ok. +// Subscribe: send {"id":1,"type":"subscribe_events","event_type":"state_changed"} +// Events: stream {"type":"event","event":{"event_type":"state_changed","data":{...}}} +// This adapter will publish to the internal fanout broker once EventService is implemented. diff --git a/ha-gateway/internal/app/entity.go b/ha-gateway/internal/app/entity.go new file mode 100644 index 0000000..59f0f08 --- /dev/null +++ b/ha-gateway/internal/app/entity.go @@ -0,0 +1,68 @@ +package app + +import ( + "context" + "fmt" + "strings" + + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain" + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/ports/driven" +) + +type EntityApp struct { + ha driven.HAClient +} + +func NewEntityApp(ha driven.HAClient) *EntityApp { + return &EntityApp{ha: ha} +} + +func (a *EntityApp) GetState(ctx context.Context, id domain.EntityID) (*domain.EntityState, error) { + s, err := a.ha.GetState(ctx, string(id)) + if err != nil { + return nil, err + } + return haStateToDomain(s), nil +} + +func (a *EntityApp) ListStates(ctx context.Context, ids []domain.EntityID, domainFilter string) ([]*domain.EntityState, error) { + all, err := a.ha.ListStates(ctx) + if err != nil { + return nil, err + } + + idSet := make(map[string]struct{}, len(ids)) + for _, id := range ids { + idSet[string(id)] = struct{}{} + } + + var out []*domain.EntityState + for _, s := range all { + if len(ids) > 0 { + if _, ok := idSet[s.EntityID]; !ok { + continue + } + } + if domainFilter != "" { + if !strings.HasPrefix(s.EntityID, domainFilter+".") { + continue + } + } + out = append(out, haStateToDomain(s)) + } + return out, nil +} + +func haStateToDomain(s *driven.HAState) *domain.EntityState { + attrs := make(map[string]string, len(s.Attributes)) + for k, v := range s.Attributes { + attrs[k] = fmt.Sprintf("%v", v) + } + return &domain.EntityState{ + EntityID: domain.EntityID(s.EntityID), + State: s.State, + Attributes: attrs, + LastChanged: s.LastChanged, + LastUpdated: s.LastUpdated, + } +} diff --git a/ha-gateway/internal/app/light.go b/ha-gateway/internal/app/light.go new file mode 100644 index 0000000..11dd2d6 --- /dev/null +++ b/ha-gateway/internal/app/light.go @@ -0,0 +1,65 @@ +package app + +import ( + "context" + + "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 +} + +func NewLightApp(ha driven.HAClient) *LightApp { + return &LightApp{ha: ha} +} + +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 { + payload["brightness_pct"] = *p.BrightnessPct + } + if p.ColorTempKelvin != nil { + payload["color_temp_kelvin"] = *p.ColorTempKelvin + } + if p.RGBColor != nil { + payload["rgb_color"] = []uint8{p.RGBColor.R, p.RGBColor.G, p.RGBColor.B} + } + if p.Transition != nil { + payload["transition"] = *p.Transition + } + return a.callService(ctx, "light", "turn_on", payload) +} + +func (a *LightApp) TurnOff(ctx context.Context, p domain.TurnOffParams) (*domain.EntityState, error) { + payload := map[string]any{"entity_id": string(p.EntityID)} + if p.Transition != nil { + payload["transition"] = *p.Transition + } + return a.callService(ctx, "light", "turn_off", payload) +} + +func (a *LightApp) Toggle(ctx context.Context, id domain.EntityID) (*domain.EntityState, error) { + payload := map[string]any{"entity_id": string(id)} + return a.callService(ctx, "light", "toggle", payload) +} + +func (a *LightApp) callService(ctx context.Context, svcDomain, service string, payload map[string]any) (*domain.EntityState, error) { + states, err := a.ha.CallService(ctx, svcDomain, service, payload) + if err != nil { + return nil, err + } + entityID, _ := payload["entity_id"].(string) + for _, s := range states { + if s.EntityID == entityID { + return haStateToDomain(s), nil + } + } + // HA may return an empty list on success; fall back to GetState. + s, err := a.ha.GetState(ctx, entityID) + if err != nil { + return nil, err + } + return haStateToDomain(s), nil +} diff --git a/ha-gateway/internal/config/config.go b/ha-gateway/internal/config/config.go new file mode 100644 index 0000000..c90b615 --- /dev/null +++ b/ha-gateway/internal/config/config.go @@ -0,0 +1,33 @@ +package config + +import ( + "errors" + "os" +) + +type Config struct { + GRPCPort string // GRPC_PORT, default "50051" + HABaseURL string // HA_BASE_URL, e.g. "http://ha.home.arpa:8123" + HAToken string // HA_TOKEN — long-lived access token (required) + OTELEndpoint string // OTEL_ENDPOINT, e.g. "otel-collector.monitoring.svc:4317" + // empty = telemetry disabled (local dev default) +} + +func Load() (*Config, error) { + token := os.Getenv("HA_TOKEN") + if token == "" { + return nil, errors.New("HA_TOKEN is required but not set") + } + + port := os.Getenv("GRPC_PORT") + if port == "" { + port = "50051" + } + + return &Config{ + GRPCPort: port, + HABaseURL: os.Getenv("HA_BASE_URL"), + HAToken: token, + OTELEndpoint: os.Getenv("OTEL_ENDPOINT"), + }, nil +} diff --git a/ha-gateway/internal/core/domain/entity.go b/ha-gateway/internal/core/domain/entity.go new file mode 100644 index 0000000..b9fed0c --- /dev/null +++ b/ha-gateway/internal/core/domain/entity.go @@ -0,0 +1,13 @@ +package domain + +import "time" + +type EntityID string + +type EntityState struct { + EntityID EntityID + State string + Attributes map[string]string + LastChanged time.Time + LastUpdated time.Time +} diff --git a/ha-gateway/internal/core/domain/light.go b/ha-gateway/internal/core/domain/light.go new file mode 100644 index 0000000..7c36b09 --- /dev/null +++ b/ha-gateway/internal/core/domain/light.go @@ -0,0 +1,18 @@ +package domain + +type TurnOnParams struct { + EntityID EntityID + BrightnessPct *uint32 // nil = not set + ColorTempKelvin *uint32 + RGBColor *RGBColor + Transition *uint32 +} + +type RGBColor struct { + R, G, B uint8 +} + +type TurnOffParams struct { + EntityID EntityID + Transition *uint32 +} diff --git a/ha-gateway/internal/core/ports/driven/ha.go b/ha-gateway/internal/core/ports/driven/ha.go new file mode 100644 index 0000000..f0589f0 --- /dev/null +++ b/ha-gateway/internal/core/ports/driven/ha.go @@ -0,0 +1,22 @@ +package driven + +import ( + "context" + "time" +) + +type HAClient interface { + GetState(ctx context.Context, entityID string) (*HAState, error) + ListStates(ctx context.Context) ([]*HAState, error) + CallService(ctx context.Context, domain, service string, payload map[string]any) ([]*HAState, error) +} + +// HAState mirrors the HA REST response. Internal type — not exposed outside +// the driven port layer. +type HAState struct { + EntityID string + State string + Attributes map[string]any + LastChanged time.Time + LastUpdated time.Time +} diff --git a/ha-gateway/internal/core/ports/driving/entity.go b/ha-gateway/internal/core/ports/driving/entity.go new file mode 100644 index 0000000..a2c4aa1 --- /dev/null +++ b/ha-gateway/internal/core/ports/driving/entity.go @@ -0,0 +1,12 @@ +package driving + +import ( + "context" + + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain" +) + +type EntityService interface { + GetState(ctx context.Context, id domain.EntityID) (*domain.EntityState, error) + ListStates(ctx context.Context, ids []domain.EntityID, domainFilter string) ([]*domain.EntityState, error) +} diff --git a/ha-gateway/internal/core/ports/driving/light.go b/ha-gateway/internal/core/ports/driving/light.go new file mode 100644 index 0000000..3d22410 --- /dev/null +++ b/ha-gateway/internal/core/ports/driving/light.go @@ -0,0 +1,13 @@ +package driving + +import ( + "context" + + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/core/domain" +) + +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) +} diff --git a/ha-gateway/internal/telemetry/telemetry.go b/ha-gateway/internal/telemetry/telemetry.go new file mode 100644 index 0000000..f8e1136 --- /dev/null +++ b/ha-gateway/internal/telemetry/telemetry.go @@ -0,0 +1,78 @@ +package telemetry + +import ( + "context" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" + "go.opentelemetry.io/otel/propagation" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + semconv "go.opentelemetry.io/otel/semconv/v1.26.0" + + "gitea.nik4nao.com/nik/home-services/ha-gateway/internal/config" +) + +// Setup initialises OTel trace and metric providers. If cfg.OTELEndpoint is +// empty, no-op providers are installed and Setup returns immediately. The +// returned shutdown func flushes and closes both exporters. +func Setup(ctx context.Context, cfg *config.Config, version string) (shutdown func(context.Context) error, err error) { + if cfg.OTELEndpoint == "" { + // Local dev — no telemetry. + return func(context.Context) error { return nil }, nil + } + + res, err := resource.New(ctx, + resource.WithAttributes( + semconv.ServiceName("ha-gateway"), + semconv.ServiceVersion(version), + ), + ) + if err != nil { + return nil, err + } + + // Trace exporter. + traceExp, err := otlptracegrpc.New(ctx, + otlptracegrpc.WithEndpoint(cfg.OTELEndpoint), + otlptracegrpc.WithInsecure(), + ) + if err != nil { + return nil, err + } + tp := sdktrace.NewTracerProvider( + sdktrace.WithBatcher(traceExp), + sdktrace.WithResource(res), + ) + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + )) + + // Metric exporter. + metricExp, err := otlpmetricgrpc.New(ctx, + otlpmetricgrpc.WithEndpoint(cfg.OTELEndpoint), + otlpmetricgrpc.WithInsecure(), + ) + if err != nil { + _ = tp.Shutdown(ctx) + return nil, err + } + mp := sdkmetric.NewMeterProvider( + sdkmetric.WithReader(sdkmetric.NewPeriodicReader(metricExp, + sdkmetric.WithInterval(30*time.Second))), + sdkmetric.WithResource(res), + ) + otel.SetMeterProvider(mp) + + return func(ctx context.Context) error { + if err := tp.Shutdown(ctx); err != nil { + return err + } + return mp.Shutdown(ctx) + }, nil +} diff --git a/proto/ha/v1/common.proto b/proto/ha/v1/common.proto new file mode 100644 index 0000000..ee53bcd --- /dev/null +++ b/proto/ha/v1/common.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +package ha.v1; +option go_package = "gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1"; + +message EntityState { + string entity_id = 1; + string state = 2; + map attributes = 3; + string last_changed = 4; // RFC3339 + string last_updated = 5; // RFC3339 +} + +message RGBColor { + uint32 r = 1; + uint32 g = 2; + uint32 b = 3; +} diff --git a/proto/ha/v1/entity.proto b/proto/ha/v1/entity.proto new file mode 100644 index 0000000..400b435 --- /dev/null +++ b/proto/ha/v1/entity.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package ha.v1; +option go_package = "gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1"; +import "ha/v1/common.proto"; + +service EntityService { + rpc GetState(GetStateRequest) returns (GetStateResponse); + rpc ListStates(ListStatesRequest) returns (ListStatesResponse); +} + +message GetStateRequest { string entity_id = 1; } +message GetStateResponse { EntityState state = 1; } + +message ListStatesRequest { + repeated string entity_ids = 1; + string domain = 2; +} +message ListStatesResponse { repeated EntityState states = 1; } diff --git a/proto/ha/v1/event.proto b/proto/ha/v1/event.proto new file mode 100644 index 0000000..22c4091 --- /dev/null +++ b/proto/ha/v1/event.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; +package ha.v1; +option go_package = "gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1"; +import "ha/v1/common.proto"; + +// TODO: implement EventService fan-out. +// Architecture: +// 1. adapters/secondary/ha/websocket.go connects to HA WebSocket, +// authenticates, and subscribes to state_changed events. +// 2. An internal broker (internal/fanout/broker.go) holds a sync.Map of +// subscriber channels, one per active Subscribe stream. +// 3. The WebSocket adapter publishes to the broker; the gRPC Subscribe +// handler reads from its channel and streams to the client. +// 4. On client disconnect (ctx.Done()), the handler deregisters its channel. +service EventService { + rpc Subscribe(SubscribeRequest) returns (stream StateChangeEvent); +} + +message SubscribeRequest { + repeated string entity_ids = 1; + repeated string domains = 2; +} + +message StateChangeEvent { + string entity_id = 1; + optional EntityState old_state = 2; // absent on first appearance + EntityState new_state = 3; + string event_time = 4; // RFC3339 +} diff --git a/proto/ha/v1/light.proto b/proto/ha/v1/light.proto new file mode 100644 index 0000000..2d59290 --- /dev/null +++ b/proto/ha/v1/light.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; +package ha.v1; +option go_package = "gitea.nik4nao.com/nik/home-services/gen/ha/v1;hav1"; +import "ha/v1/common.proto"; + +service LightService { + rpc TurnOn(TurnOnRequest) returns (LightResponse); + rpc TurnOff(TurnOffRequest) returns (LightResponse); + rpc Toggle(ToggleRequest) returns (LightResponse); +} + +// optional fields require protobuf 3.15+ / buf >= 1.0. They generate +// pointer fields in Go with Has*() accessor methods. This is intentional — +// it lets the gateway distinguish "brightness not set" from "brightness = 0". +message TurnOnRequest { + string entity_id = 1; + optional uint32 brightness_pct = 2; // 0–100 + optional uint32 color_temp_kelvin = 3; // e.g. 2700–6500 + optional RGBColor rgb_color = 4; // ignored if color_temp_kelvin set + optional uint32 transition = 5; // seconds +} +message TurnOffRequest { + string entity_id = 1; + optional uint32 transition = 2; +} +message ToggleRequest { string entity_id = 1; } +message LightResponse { EntityState state = 1; } diff --git a/proto/ha/v1/switch.proto b/proto/ha/v1/switch.proto new file mode 100644 index 0000000..4d38d80 --- /dev/null +++ b/proto/ha/v1/switch.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +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); +} + +message SwitchRequest { string entity_id = 1; } +message SwitchResponse { EntityState state = 1; }