Added new app, ha-gateway

This commit is contained in:
Nik Afiq 2026-03-25 19:52:15 +09:00
parent 3d9690e939
commit 2e99c464ff
41 changed files with 3195 additions and 0 deletions

10
buf.gen.yaml Normal file
View File

@ -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

3
buf.yaml Normal file
View File

@ -0,0 +1,3 @@
version: v2
modules:
- path: proto

15
gen/go.mod Normal file
View File

@ -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
)

38
gen/go.sum Normal file
View File

@ -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=

230
gen/ha/v1/common.pb.go Normal file
View File

@ -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
}

285
gen/ha/v1/entity.pb.go Normal file
View File

@ -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
}

159
gen/ha/v1/entity_grpc.pb.go Normal file
View File

@ -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",
}

218
gen/ha/v1/event.pb.go Normal file
View File

@ -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
}

144
gen/ha/v1/event_grpc.pb.go Normal file
View File

@ -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",
}

338
gen/ha/v1/light.pb.go Normal file
View File

@ -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"` // 0100
ColorTempKelvin *uint32 `protobuf:"varint,3,opt,name=color_temp_kelvin,json=colorTempKelvin,proto3,oneof" json:"color_temp_kelvin,omitempty"` // e.g. 27006500
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
}

197
gen/ha/v1/light_grpc.pb.go Normal file
View File

@ -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",
}

182
gen/ha/v1/switch.pb.go Normal file
View File

@ -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
}

209
gen/ha/v1/switch_grpc.pb.go Normal file
View File

@ -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",
}

6
go.work Normal file
View File

@ -0,0 +1,6 @@
go 1.26
use (
./gen
./ha-gateway
)

92
go.work.sum Normal file
View File

@ -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=

4
ha-gateway/.env.example Normal file
View File

@ -0,0 +1,4 @@
GRPC_PORT=50051
HA_BASE_URL=http://ha.home.arpa:8123
HA_TOKEN=your-long-lived-token-here
OTEL_ENDPOINT=

19
ha-gateway/Dockerfile Normal file
View File

@ -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"]

View File

@ -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=<tag>".
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
}

37
ha-gateway/go.mod Normal file
View File

@ -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

67
ha-gateway/go.sum Normal file
View File

@ -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=

View File

@ -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")

View File

@ -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.

View File

@ -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
}

View File

@ -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
}

View File

@ -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.

View File

@ -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
}

View File

@ -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.

View File

@ -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,
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

17
proto/ha/v1/common.proto Normal file
View File

@ -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<string, string> attributes = 3;
string last_changed = 4; // RFC3339
string last_updated = 5; // RFC3339
}
message RGBColor {
uint32 r = 1;
uint32 g = 2;
uint32 b = 3;
}

18
proto/ha/v1/entity.proto Normal file
View File

@ -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; }

29
proto/ha/v1/event.proto Normal file
View File

@ -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
}

27
proto/ha/v1/light.proto Normal file
View File

@ -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; // 0100
optional uint32 color_temp_kelvin = 3; // e.g. 27006500
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; }

18
proto/ha/v1/switch.proto Normal file
View File

@ -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; }