├── .gitignore ├── versions.txt ├── cli ├── command │ ├── device │ │ ├── get.go │ │ ├── set.go │ │ ├── set_entities.go │ │ ├── device.go │ │ └── get_entities.go │ └── root │ │ ├── verson.go │ │ ├── login.go │ │ └── cmd.go ├── main.go ├── version │ └── version.go └── types │ └── config.go ├── proto ├── generate.sh ├── README.md ├── api_options.proto └── api.proto ├── pkg ├── connection │ ├── types.go │ ├── conn_plaintext.go │ └── conn_encryption.go ├── types │ └── types.go ├── client │ └── client.go └── api │ ├── api_options.pb.go │ └── helper.go ├── .github └── workflows │ ├── lint.yaml │ └── publish_executables.yaml ├── examples ├── device_info │ └── main.go ├── tail_logs │ └── main.go ├── camera │ └── main.go └── commom.go ├── scripts ├── version.sh └── generate_executables.sh ├── CHANGELOG.md ├── go.mod ├── README.md ├── go.sum └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /esphome_config 2 | /.esphome/ 3 | /secrets.yaml 4 | -------------------------------------------------------------------------------- /versions.txt: -------------------------------------------------------------------------------- 1 | # this version number will be used for development builds 2 | esphomectl=1.4 -------------------------------------------------------------------------------- /cli/command/device/get.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | rootCmd "github.com/mycontroller-org/esphome_api/cli/command/root" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func init() { 9 | rootCmd.AddCommand(getCmd) 10 | } 11 | 12 | var getCmd = &cobra.Command{ 13 | Use: "get", 14 | Short: "List resources", 15 | } 16 | -------------------------------------------------------------------------------- /cli/command/device/set.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | rootCmd "github.com/mycontroller-org/esphome_api/cli/command/root" 5 | "github.com/spf13/cobra" 6 | ) 7 | 8 | func init() { 9 | rootCmd.AddCommand(setCmd) 10 | } 11 | 12 | var setCmd = &cobra.Command{ 13 | Use: "set", 14 | Short: "update resource state", 15 | } 16 | -------------------------------------------------------------------------------- /cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "github.com/mycontroller-org/esphome_api/cli/command/device" 5 | rootCmd "github.com/mycontroller-org/esphome_api/cli/command/root" 6 | 7 | clientTY "github.com/mycontroller-org/server/v2/pkg/types/client" 8 | ) 9 | 10 | func main() { 11 | rootCmd.Execute(clientTY.NewStdStreams()) 12 | } 13 | -------------------------------------------------------------------------------- /proto/generate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # pre-requests 4 | # go get -u google.golang.org/protobuf/cmd/protoc-gen-go 5 | # go install google.golang.org/protobuf/cmd/protoc-gen-go 6 | 7 | OUT_DIR=../pkg/api 8 | mkdir -p ${OUT_DIR} 9 | 10 | protoc \ 11 | --go_out=${OUT_DIR} --go_opt=paths=source_relative \ 12 | --go_opt=Mapi.proto=pkg/api \ 13 | --go_opt=Mapi_options.proto=pkg/api \ 14 | *.proto 15 | -------------------------------------------------------------------------------- /cli/command/root/verson.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/mycontroller-org/esphome_api/cli/version" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func init() { 11 | AddCommand(versionCmd) 12 | } 13 | 14 | var versionCmd = &cobra.Command{ 15 | Use: "version", 16 | Long: "Prints version information", 17 | Run: func(cmd *cobra.Command, args []string) { 18 | fmt.Println(version.Get().String()) 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /proto/README.md: -------------------------------------------------------------------------------- 1 | Proto files are copied from the [esphome GitHub repository](https://github.com/esphome/esphome) 2 | 3 | * [api.proto](https://github.com/esphome/esphome/blob/dev/esphome/components/api/api.proto) 4 | * [api_options.proto](https://github.com/esphome/esphome/blob/dev/esphome/components/api/api_options.proto) 5 | 6 | ### Steps to update the generated file 7 | * Install [protoc](https://github.com/protocolbuffers/protobuf) 8 | * `google/protobuf/descriptor.proto` should be on the import path 9 | * Execute the following script 10 | ```bash 11 | cd proto 12 | ./generate.sh 13 | ``` -------------------------------------------------------------------------------- /pkg/connection/types.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "time" 7 | 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | type ApiConnection interface { 12 | Write(message proto.Message) error 13 | Read(reader *bufio.Reader) (proto.Message, error) 14 | Handshake() error 15 | } 16 | 17 | func GetConnection(conn net.Conn, communicationTimeout time.Duration, encryptionKey string) (ApiConnection, error) { 18 | if encryptionKey != "" { 19 | return NewEncryptedConnection(conn, communicationTimeout, encryptionKey) 20 | } 21 | return NewPlaintextConnection(conn, communicationTimeout) 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ["v*"] 6 | pull_request: 7 | 8 | jobs: 9 | golang-lint: 10 | name: lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-go@v2 15 | with: 16 | go-version: ^1.21 17 | 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v2 20 | with: 21 | version: latest 22 | skip-go-installation: true 23 | # Optional: show only new issues if it's a pull request. The default value is `false`. 24 | # only-new-issues: true 25 | -------------------------------------------------------------------------------- /examples/device_info/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | examples "github.com/mycontroller-org/esphome_api/examples" 8 | ) 9 | 10 | func main() { 11 | client, err := examples.GetClient(nil) 12 | if err != nil { 13 | log.Fatalln(err) 14 | } 15 | defer client.Close() 16 | 17 | err = client.Ping() 18 | if err != nil { 19 | log.Fatalln(err) 20 | } 21 | 22 | hr, err := client.Hello() 23 | if err != nil { 24 | log.Fatalln(err) 25 | } 26 | 27 | di, err := client.DeviceInfo() 28 | if err != nil { 29 | log.Fatalln(err) 30 | } 31 | 32 | fmt.Println("HelloResponse:", hr.String()) 33 | fmt.Println("DeviceInfo:", di.String()) 34 | } 35 | -------------------------------------------------------------------------------- /cli/command/device/set_entities.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | ) 6 | 7 | func init() { 8 | setCmd.AddCommand(setEntitiesCmd) 9 | } 10 | 11 | var setEntitiesCmd = &cobra.Command{ 12 | Use: "entity", 13 | Short: "Updates entity value", 14 | Example: ` # lists available entities 15 | esphomectl set entities unique_id --payload test=on 16 | `, 17 | Args: cobra.MinimumNArgs(1), 18 | Run: func(cmd *cobra.Command, args []string) { 19 | // _client, err := rootCmd.GetActiveClient(nil) 20 | // if err != nil { 21 | // fmt.Fprintln(cmd.ErrOrStderr(), "error:", err.Error()) 22 | // return 23 | // } 24 | // 25 | // var request proto.Message 26 | 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /proto/api_options.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | import "google/protobuf/descriptor.proto"; 3 | 4 | 5 | enum APISourceType { 6 | SOURCE_BOTH = 0; 7 | SOURCE_SERVER = 1; 8 | SOURCE_CLIENT = 2; 9 | } 10 | 11 | message void {} 12 | 13 | extend google.protobuf.MethodOptions { 14 | optional bool needs_setup_connection = 1038 [default=true]; 15 | optional bool needs_authentication = 1039 [default=true]; 16 | } 17 | 18 | extend google.protobuf.MessageOptions { 19 | optional uint32 id = 1036 [default=0]; 20 | optional APISourceType source = 1037 [default=SOURCE_BOTH]; 21 | optional string ifdef = 1038; 22 | optional bool log = 1039 [default=true]; 23 | optional bool no_delay = 1040 [default=false]; 24 | } -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this script updateds version information 4 | 5 | # version details 6 | export BUILD_DATE=`date -u +'%Y-%m-%dT%H:%M:%S%:z'` 7 | export GIT_BRANCH=`git rev-parse --abbrev-ref HEAD` 8 | export GIT_SHA=`git rev-parse HEAD` 9 | export GIT_SHA_SHORT=`git rev-parse --short HEAD` 10 | export VERSION_PKG="github.com/mycontroller-org/esphome_api/cli/version" 11 | 12 | # update tag, if available 13 | if [ ${GIT_BRANCH} = "HEAD" ]; then 14 | export GIT_BRANCH=`git describe --abbrev=0 --tags` 15 | fi 16 | 17 | # update version number 18 | export VERSION=`echo ${GIT_BRANCH} | awk 'match($0, /([0-9]*\.[0-9]*)$/) { print substr($0, RSTART, RLENGTH) }'` 19 | if [ -z "$VERSION" ]; then 20 | # takes version from versions file and adds devel suffix with that 21 | STATIC_VERSION=`grep esphomectl= versions.txt | awk -F= '{print $2}'` 22 | export VERSION="${STATIC_VERSION}-devel" 23 | fi 24 | 25 | export LD_FLAGS="-X $VERSION_PKG.version=$VERSION -X $VERSION_PKG.buildDate=$BUILD_DATE -X $VERSION_PKG.gitCommit=$GIT_SHA" 26 | -------------------------------------------------------------------------------- /examples/tail_logs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | examples "github.com/mycontroller-org/esphome_api/examples" 10 | types "github.com/mycontroller-org/esphome_api/pkg/types" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | func main() { 15 | var logLevel = flag.Int("log_level", int(types.LogLevelVeryVerbose), "log level (0-6)") 16 | var monitoringDuration = flag.Duration("monitoring_duration", 30*time.Second, "monitoring duration") 17 | flag.Parse() 18 | 19 | client, err := examples.GetClient(handlerFuncImpl) 20 | if err != nil { 21 | log.Fatalln(err) 22 | } 23 | defer client.Close() 24 | 25 | fmt.Println("Listening logs, will terminate in", *monitoringDuration) 26 | 27 | err = client.SubscribeLogs(types.LogLevel(*logLevel)) 28 | if err != nil { 29 | log.Fatalln(err) 30 | } 31 | 32 | <-time.After(*monitoringDuration) 33 | } 34 | 35 | func handlerFuncImpl(msg proto.Message) { 36 | log, err := types.GetLogEntry(msg) 37 | if err != nil { 38 | fmt.Printf("MSG: %+v\n", msg) 39 | } 40 | fmt.Println("Log:", log.String()) 41 | } 42 | -------------------------------------------------------------------------------- /cli/version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | var ( 9 | gitCommit string 10 | version string 11 | buildDate string 12 | ) 13 | 14 | // Version holds version data 15 | type Version struct { 16 | Version string `json:"version"` 17 | GitCommit string `json:"gitCommit"` 18 | BuildDate string `json:"buildDate"` 19 | GoVersion string `json:"goVersion"` 20 | Compiler string `json:"compiler"` 21 | Platform string `json:"platform"` 22 | Arch string `json:"arch"` 23 | } 24 | 25 | // Get returns the Version object 26 | func Get() *Version { 27 | return &Version{ 28 | GitCommit: gitCommit, 29 | Version: version, 30 | BuildDate: buildDate, 31 | GoVersion: runtime.Version(), 32 | Compiler: runtime.Compiler, 33 | Platform: runtime.GOOS, 34 | Arch: runtime.GOARCH, 35 | } 36 | } 37 | 38 | func (v *Version) String() string { 39 | return fmt.Sprintf("{version:%s, gitCommit:%s, buildDate:%s, goVersion:%s, compiler:%s, platform:%s, arch:%s}", 40 | v.Version, v.GitCommit, v.BuildDate, v.GoVersion, v.Compiler, v.Platform, v.Arch, 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changes by Version 2 | 3 | ### v1.3.0 (2023-01-06) 4 | * update proto: bluetooth support included ([#7](https://github.com/mycontroller-org/esphome_api/pull/7), [@jkandasa](https://github.com/jkandasa)) 5 | * support encrypted connection ([#5](https://github.com/mycontroller-org/esphome_api/pull/5), [@jkandasa](https://github.com/jkandasa)) 6 | 7 | **Contains BREAKING CHANGES** 8 | * get new client function name changed and encryption key argument added on the new function 9 | ``` 10 | old: func Init(clientID, address string, timeout time.Duration, handlerFunc func(proto.Message)) (*Client, error) 11 | new: GetClient(clientID, address, encryptionKey string, timeout time.Duration, handlerFunc func(proto.Message)) (*Client, error) 12 | ``` 13 | 14 | ### v1.2.0 (2022-07-06) 15 | * rename pkg model to types, upgrade go version ([#2](https://github.com/mycontroller-org/esphome_api/pull/2), [@jkandasa](https://github.com/jkandasa)) **Contains BREAKING CHANGES** 16 | ### v1.1.0 (2022-06-13) 17 | * Updated api.proto and included new message definitions ([#1](https://github.com/mycontroller-org/esphome_api/pull/1), [@mligor](https://github.com/mligor)) 18 | 19 | 20 | ### v1.0.0 (2022-06-13) 21 | * initial release 22 | -------------------------------------------------------------------------------- /examples/camera/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/fs" 7 | "io/ioutil" 8 | "log" 9 | "time" 10 | 11 | examples "github.com/mycontroller-org/esphome_api/examples" 12 | "github.com/mycontroller-org/esphome_api/pkg/api" 13 | "google.golang.org/protobuf/proto" 14 | ) 15 | 16 | func main() { 17 | client, err := examples.GetClient(handleFuncImpl) 18 | if err != nil { 19 | log.Fatalln(err) 20 | } 21 | defer client.Close() 22 | 23 | // subscribe state changes 24 | client.Send(&api.SubscribeStatesRequest{}) 25 | 26 | // wait for a second 27 | <-time.After(1 * time.Second) 28 | 29 | // take a picture 30 | client.Send(&api.CameraImageRequest{Single: true}) 31 | 32 | // wait 10 seconds 33 | <-time.After(3 * time.Second) 34 | 35 | // if image received, convert it to jpeg 36 | if received { 37 | err = ioutil.WriteFile("camera_image.jpeg", buffer.Bytes(), fs.ModePerm) 38 | if err != nil { 39 | fmt.Println(err) 40 | } 41 | } 42 | } 43 | 44 | var ( 45 | buffer = new(bytes.Buffer) 46 | received = false 47 | ) 48 | 49 | func handleFuncImpl(msg proto.Message) { 50 | switch msg := msg.(type) { 51 | case *api.CameraImageResponse: 52 | if !received { 53 | buffer.Write(msg.Data) 54 | if msg.Done { 55 | received = true 56 | fmt.Println("Image received") 57 | } 58 | } 59 | 60 | default: 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mycontroller-org/esphome_api 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/flynn/noise v1.0.1-0.20220214164934-d803f5c4b0f4 7 | github.com/mitchellh/go-homedir v1.1.0 8 | github.com/mycontroller-org/server/v2 v2.0.1-0.20240615005612-617ab65354e9 9 | github.com/spf13/cobra v1.8.1 10 | github.com/spf13/viper v1.19.0 11 | google.golang.org/protobuf v1.34.2 12 | gopkg.in/yaml.v3 v3.0.1 13 | ) 14 | 15 | require ( 16 | github.com/fsnotify/fsnotify v1.7.0 // indirect 17 | github.com/hashicorp/hcl v1.0.0 // indirect 18 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 19 | github.com/magiconair/properties v1.8.7 // indirect 20 | github.com/mattn/go-runewidth v0.0.9 // indirect 21 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect 22 | github.com/nleeper/goment v1.4.4 // indirect 23 | github.com/olekukonko/tablewriter v0.0.5 // indirect 24 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 25 | github.com/sagikazarmark/locafero v0.4.0 // indirect 26 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 27 | github.com/sourcegraph/conc v0.3.0 // indirect 28 | github.com/spf13/afero v1.11.0 // indirect 29 | github.com/spf13/cast v1.6.0 // indirect 30 | github.com/spf13/pflag v1.0.5 // indirect 31 | github.com/subosito/gotenv v1.6.0 // indirect 32 | github.com/tkuchiki/go-timezone v0.2.0 // indirect 33 | go.uber.org/multierr v1.11.0 // indirect 34 | go.uber.org/zap v1.27.0 // indirect 35 | golang.org/x/crypto v0.24.0 // indirect 36 | golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect 37 | golang.org/x/sys v0.21.0 // indirect 38 | golang.org/x/text v0.16.0 // indirect 39 | gopkg.in/ini.v1 v1.67.0 // indirect 40 | ) 41 | -------------------------------------------------------------------------------- /examples/commom.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "time" 8 | 9 | esphome "github.com/mycontroller-org/esphome_api/pkg/client" 10 | "google.golang.org/protobuf/proto" 11 | ) 12 | 13 | const ( 14 | EnvHostAddress = "ESPHOME_ADDRESS" 15 | EnvPassword = "ESPHOME_PASSWORD" 16 | EnvEncryptionKey = "ESPHOME_ENCRYPTION_KEY" 17 | ) 18 | 19 | var ( 20 | HostAddressFlag = flag.String("address", "", "esphome node hostname or IP with port. example: my_esphome.local:6053") 21 | PasswordFlag = flag.String("password", "", "esphome node API password") 22 | EncryptionKeyFlag = flag.String("encryption-key", "", "esphome node API encryption key") 23 | TimeoutFlag = flag.Duration("timeout", 10*time.Second, "communication timeout") 24 | ) 25 | 26 | func GetClient(handlerFunc func(msg proto.Message)) (*esphome.Client, error) { 27 | flag.Parse() 28 | 29 | // update hostaddress 30 | if *HostAddressFlag == "" { 31 | if os.Getenv(EnvHostAddress) != "" { 32 | *HostAddressFlag = os.Getenv(EnvHostAddress) 33 | } else { 34 | *HostAddressFlag = "esphome.local:6053" 35 | } 36 | } 37 | 38 | // update password 39 | if *PasswordFlag == "" { 40 | *PasswordFlag = os.Getenv(EnvPassword) 41 | } 42 | 43 | // update encryption key 44 | if *EncryptionKeyFlag == "" { 45 | *EncryptionKeyFlag = os.Getenv(EnvEncryptionKey) 46 | } 47 | 48 | if handlerFunc == nil { 49 | handlerFunc = handlerFuncImpl 50 | } 51 | 52 | client, err := esphome.GetClient("mycontroller.org", *HostAddressFlag, *EncryptionKeyFlag, *TimeoutFlag, handlerFunc) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | if err = client.Login(*PasswordFlag); err != nil { 58 | _ = client.Close() 59 | return nil, err 60 | } 61 | 62 | return client, nil 63 | } 64 | 65 | func handlerFuncImpl(msg proto.Message) { 66 | fmt.Printf("received a message, type: %T, value: [%v]\n", msg, msg) 67 | } 68 | -------------------------------------------------------------------------------- /scripts/generate_executables.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # this script used to generate binary files 4 | # should be executed from the root locations of the repository 5 | 6 | 7 | source ./scripts/version.sh 8 | 9 | BUILD_DIR=builds 10 | BINARY_DIR=binary 11 | # clean builds directory 12 | rm ${BUILD_DIR}/* -rf 13 | 14 | 15 | # create directories 16 | mkdir -p ${BUILD_DIR}/${BINARY_DIR} 17 | 18 | # download dependencies 19 | go mod tidy 20 | 21 | 22 | function package { 23 | local PACKAGE_STAGING_DIR=$1 24 | local BINARY_FILE=$2 25 | local FILE_EXTENSION=$3 26 | 27 | mkdir -p ${PACKAGE_STAGING_DIR} 28 | 29 | # echo "Package dir: ${PACKAGE_STAGING_DIR}" 30 | cp ${BUILD_DIR}/${BINARY_DIR}/${BINARY_FILE} ${PACKAGE_STAGING_DIR}/esphomectl${FILE_EXTENSION} 31 | 32 | # copy license 33 | cp LICENSE ${PACKAGE_STAGING_DIR}/LICENSE.txt 34 | 35 | if [[ ${PACKAGE_STAGING_DIR} =~ "windows" ]]; then 36 | ARCHIVE_NAME="${PACKAGE_STAGING_DIR}.zip" 37 | zip -r ${BUILD_DIR}/${ARCHIVE_NAME} ${PACKAGE_STAGING_DIR} 38 | else 39 | ARCHIVE_NAME="${PACKAGE_STAGING_DIR}.tar.gz" 40 | tar -czf ${BUILD_DIR}/${ARCHIVE_NAME} ${PACKAGE_STAGING_DIR} 41 | fi 42 | rm ${PACKAGE_STAGING_DIR} -rf 43 | } 44 | 45 | # platforms to build 46 | PLATFORMS=("linux/arm" "linux/arm64" "linux/386" "linux/amd64" "windows/386" "windows/amd64") 47 | 48 | # compile 49 | for platform in "${PLATFORMS[@]}" 50 | do 51 | platform_raw=(${platform//\// }) 52 | GOOS=${platform_raw[0]} 53 | GOARCH=${platform_raw[1]} 54 | package_name="esphomectl-${GOOS}-${GOARCH}" 55 | 56 | env GOOS=${GOOS} GOARCH=${GOARCH} go build -o ${BUILD_DIR}/${BINARY_DIR}/${package_name} -ldflags "-s -w $LD_FLAGS" cli/main.go 57 | if [ $? -ne 0 ]; then 58 | echo 'an error has occurred. aborting the build process' 59 | exit 1 60 | fi 61 | 62 | FILE_EXTENSION="" 63 | if [ $GOOS = "windows" ]; then 64 | FILE_EXTENSION='.exe' 65 | fi 66 | 67 | package esphomectl-${VERSION}-${GOOS}-${GOARCH} ${package_name} ${FILE_EXTENSION} 68 | done 69 | -------------------------------------------------------------------------------- /.github/workflows/publish_executables.yaml: -------------------------------------------------------------------------------- 1 | name: publish executables 2 | on: 3 | push: 4 | branches: [main] 5 | tags: ["v*"] 6 | 7 | jobs: 8 | setup: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: checkout the source code 13 | uses: actions/checkout@v2 14 | 15 | - uses: actions/setup-go@v2 16 | with: 17 | go-version: ^1.21 18 | 19 | - name: Cache go modules 20 | uses: actions/cache@v2 21 | with: 22 | path: ~/go/pkg/mod 23 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 24 | restore-keys: | 25 | ${{ runner.os }}-go- 26 | 27 | - name: build executable bundles 28 | run: ./scripts/generate_executables.sh 29 | 30 | - name: generate a build timestamp and sha256sum files 31 | run: | 32 | cd builds 33 | echo `date -u +'%Y%m%d%H%M%S'` > ./build_timestamp.txt 34 | echo `date -u +'%Y-%m-%dT%H:%M:%S%:z'` >> ./build_timestamp.txt 35 | sha256sum *.tar.gz > ./SHA256SUMS.txt 36 | sha256sum *.zip >> ./SHA256SUMS.txt 37 | 38 | - name: update release notes and executables 39 | if: startsWith(github.ref, 'refs/tags/') # executes only for new release 40 | uses: softprops/action-gh-release@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 43 | with: 44 | files: | 45 | builds/*.tar.gz 46 | builds/*.zip 47 | builds/build_timestamp.txt 48 | builds/SHA256SUMS.txt 49 | 50 | - name: Update executables for main branch changes 51 | if: startsWith(github.ref, 'refs/heads/main') # executes only for changes in main 52 | uses: "marvinpinto/action-automatic-releases@latest" 53 | with: 54 | repo_token: "${{ secrets.GH_TOKEN }}" 55 | automatic_release_tag: main-devel 56 | prerelease: true 57 | title: "Development Build - Pre Release" 58 | files: | 59 | builds/*.tar.gz 60 | builds/*.zip 61 | builds/build_timestamp.txt 62 | builds/SHA256SUMS.txt 63 | -------------------------------------------------------------------------------- /pkg/types/types.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/mycontroller-org/esphome_api/pkg/api" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | // Error types 12 | var ( 13 | ErrPassword = errors.New("esphome_api: invalid password") 14 | ErrCommunicationTimeout = errors.New("esphome_api: communication timeout") 15 | ErrConnRequireEncryption = errors.New("esphome_api: connection requires encryption") 16 | ) 17 | 18 | // call back function used to report received messages 19 | type CallBackFunc func(proto.Message) 20 | 21 | // DeviceInfo struct 22 | type DeviceInfo struct { 23 | Name string 24 | Model string 25 | MacAddress string 26 | EsphomeVersion string 27 | CompilationTime string 28 | UsesPassword bool 29 | HasDeepSleep bool 30 | } 31 | 32 | func (di *DeviceInfo) String() string { 33 | return fmt.Sprintf("{name: %v, model:%v, mac_address:%v, esphome_version:%v, compilation_time:%v, uses_password:%v, has_deep_sleep:%v}", 34 | di.Name, di.Model, di.MacAddress, di.EsphomeVersion, di.CompilationTime, di.UsesPassword, di.HasDeepSleep) 35 | } 36 | 37 | // LogLevel type 38 | type LogLevel int32 39 | 40 | // log levels 41 | const ( 42 | LogLevelNone LogLevel = iota 43 | LogLevelError 44 | LogLevelWarn 45 | LogLevelInfo 46 | LogLevelDebug // default 47 | LogLevelVerbose 48 | LogLevelVeryVerbose 49 | ) 50 | 51 | // LogEntry of a message 52 | type LogEntry struct { 53 | Level LogLevel 54 | Tag string 55 | Message string 56 | SendFailed bool 57 | } 58 | 59 | func (le *LogEntry) String() string { 60 | return fmt.Sprintf("{level: %v, tag:%v, send_failed:%v, message:[%v]}", 61 | le.Level, le.Tag, le.SendFailed, le.Message) 62 | } 63 | 64 | func GetLogEntry(msg proto.Message) (*LogEntry, error) { 65 | entry, ok := msg.(*api.SubscribeLogsResponse) 66 | if !ok { 67 | return nil, fmt.Errorf("received invalid data type:%T", msg) 68 | } 69 | log := LogEntry{ 70 | Level: LogLevel(entry.Level), 71 | Message: entry.Message, 72 | SendFailed: entry.SendFailed, 73 | } 74 | return &log, nil 75 | } 76 | 77 | type HelloResponse struct { 78 | ApiVersionMajor uint32 79 | ApiVersionMinor uint32 80 | ServerInfo string 81 | Name string 82 | } 83 | 84 | func (hr *HelloResponse) String() string { 85 | return fmt.Sprintf("{name: %v, api_version_major: %v, api_version_minor:%v, server_info:%v}", 86 | hr.Name, hr.ApiVersionMajor, hr.ApiVersionMinor, hr.ServerInfo) 87 | } 88 | -------------------------------------------------------------------------------- /cli/types/config.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | const ( 12 | EncodePrefix = "BASE64/" 13 | ) 14 | 15 | type Config struct { 16 | Active string `yaml:"active"` 17 | Devices []DeviceConfig `yaml:"devices"` 18 | } 19 | 20 | type DeviceInfo struct { 21 | Name string `yaml:"name"` 22 | Model string `yaml:"model"` 23 | MacAddress string `yaml:"macAddress"` 24 | EsphomeVersion string `yaml:"esphomeVersion"` 25 | CompilationTime string `yaml:"compilationTime"` 26 | UsesPassword bool `yaml:"usesPassword"` 27 | HasDeepSleep bool `yaml:"hasDeepSleep"` 28 | StatusOn time.Time `yaml:"statusOn"` 29 | } 30 | 31 | func (di *DeviceInfo) Clone() DeviceInfo { 32 | return DeviceInfo{ 33 | Name: di.Name, 34 | Model: di.Model, 35 | MacAddress: di.MacAddress, 36 | EsphomeVersion: di.EsphomeVersion, 37 | CompilationTime: di.CompilationTime, 38 | UsesPassword: di.UsesPassword, 39 | HasDeepSleep: di.HasDeepSleep, 40 | } 41 | } 42 | 43 | type DeviceConfig struct { 44 | Address string `yaml:"address"` 45 | Password string `yaml:"password"` // encode as base64 46 | EncryptionKey string `yaml:"encryptionKey"` 47 | Timeout time.Duration `yaml:"timeout"` 48 | Info DeviceInfo `yaml:"info"` 49 | } 50 | 51 | func (dc *DeviceConfig) Clone() DeviceConfig { 52 | return DeviceConfig{ 53 | Address: dc.Address, 54 | Password: dc.Password, 55 | EncryptionKey: dc.EncryptionKey, 56 | Timeout: dc.Timeout, 57 | Info: dc.Info.Clone(), 58 | } 59 | } 60 | 61 | // GetPassword decodes and returns the password 62 | func (nc *DeviceConfig) GetPassword() string { 63 | if strings.HasPrefix(nc.Password, EncodePrefix) { 64 | password := strings.Replace(nc.Password, EncodePrefix, "", 1) 65 | decodedPassword, err := base64.StdEncoding.DecodeString(password) 66 | if err != nil { 67 | log.Fatal("error on decoding the password", err) 68 | } 69 | return string(decodedPassword) 70 | } 71 | return nc.Password 72 | } 73 | 74 | // EncodePassword encodes and update the password 75 | func (nc *DeviceConfig) EncodePassword() { 76 | if nc.Password != "" && !strings.HasPrefix(nc.Password, EncodePrefix) { 77 | encodedPassword := base64.StdEncoding.EncodeToString([]byte(nc.Password)) 78 | nc.Password = fmt.Sprintf("%s%s", EncodePrefix, encodedPassword) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /cli/command/device/device.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | rootCmd "github.com/mycontroller-org/esphome_api/cli/command/root" 8 | "github.com/mycontroller-org/server/v2/pkg/utils/printer" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | rootCmd.AddCommand(deviceContextCmd) 14 | rootCmd.AddCommand(getDevicesCmd) 15 | } 16 | 17 | var deviceContextCmd = &cobra.Command{ 18 | Use: "device", 19 | Short: "Switch or set a device", 20 | Example: ` # set a node 21 | esphomectl device my-device-1 22 | 23 | # get the active device 24 | esphomectl device 25 | `, 26 | Args: cobra.MaximumNArgs(1), 27 | Run: func(cmd *cobra.Command, args []string) { 28 | if len(args) == 0 { 29 | if rootCmd.CONFIG.Active == "" { 30 | fmt.Fprintln(cmd.OutOrStdout(), "No resource found") 31 | return 32 | } 33 | fmt.Fprintf(cmd.ErrOrStderr(), "Active node '%s'\n", rootCmd.CONFIG.Active) 34 | return 35 | } 36 | rootCmd.CONFIG.Active = strings.TrimSpace(args[0]) 37 | client, err := rootCmd.GetActiveClient(nil) 38 | if err != nil { 39 | fmt.Fprintln(cmd.ErrOrStderr(), "Error on login", err) 40 | return 41 | } 42 | if client != nil { 43 | rootCmd.WriteConfigFile() 44 | fmt.Fprintf(cmd.OutOrStdout(), "Switched to '%s'\n", rootCmd.CONFIG.Active) 45 | } 46 | }, 47 | } 48 | 49 | var getDevicesCmd = &cobra.Command{ 50 | Use: "devices", 51 | Short: "Display configured devices", 52 | Example: ` # display configured devices 53 | esphomectl devices 54 | `, 55 | Run: func(cmd *cobra.Command, args []string) { 56 | if len(rootCmd.CONFIG.Devices) == 0 { 57 | fmt.Fprintln(cmd.OutOrStdout(), "No resource found") 58 | return 59 | } 60 | headers := []printer.Header{ 61 | {Title: "address", ValuePath: "address"}, 62 | {Title: "name", ValuePath: "info.name"}, 63 | {Title: "model", ValuePath: "info.model"}, 64 | {Title: "mac address", ValuePath: "info.macAddress"}, 65 | {Title: "version", ValuePath: "info.esphomeVersion"}, 66 | {Title: "compilation time", ValuePath: "info.compilationTime"}, 67 | {Title: "uses password", ValuePath: "info.usesPassword"}, 68 | {Title: "has deep sleep", ValuePath: "info.hasDeepSleep"}, 69 | {Title: "timeout", ValuePath: "timeout", IsWide: true}, 70 | {Title: "status on", ValuePath: "info.statusOn", DisplayStyle: printer.DisplayStyleRelativeTime}, 71 | } 72 | data := make([]interface{}, 0) 73 | for _, device := range rootCmd.CONFIG.Devices { 74 | data = append(data, device) 75 | } 76 | printer.Print(cmd.OutOrStdout(), headers, data, rootCmd.HideHeader, rootCmd.OutputFormat, rootCmd.Pretty) 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /pkg/connection/conn_plaintext.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "bufio" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "sync" 11 | "time" 12 | 13 | "github.com/mycontroller-org/esphome_api/pkg/api" 14 | types "github.com/mycontroller-org/esphome_api/pkg/types" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | type PlaintextConnection struct { 19 | conn net.Conn 20 | CommunicationTimeout time.Duration 21 | writeMutex sync.Mutex 22 | readMutex sync.Mutex 23 | } 24 | 25 | func NewPlaintextConnection(conn net.Conn, communicationTimeout time.Duration) (ApiConnection, error) { 26 | ptc := &PlaintextConnection{ 27 | conn: conn, 28 | CommunicationTimeout: communicationTimeout, 29 | } 30 | return ptc, nil 31 | } 32 | 33 | func (ptc *PlaintextConnection) Handshake() error { 34 | return nil 35 | } 36 | 37 | func (ptc *PlaintextConnection) Write(message proto.Message) error { 38 | messageBytes, err := proto.Marshal(message) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | // preamble byte + message length(max 2 bytes) + message type id (max 2 bytes) 44 | header := make([]byte, 5) 45 | 46 | // preamble byte 47 | header[0] = 0x00 48 | 49 | index := 1 50 | // include message bytes length 51 | index += binary.PutUvarint(header[index:], uint64(len(messageBytes))) 52 | 53 | // include message type 54 | index += binary.PutUvarint(header[index:], api.TypeID(message)) 55 | 56 | packed := append(header[:index], messageBytes...) 57 | 58 | // set write lock 59 | ptc.writeMutex.Lock() 60 | defer ptc.writeMutex.Unlock() 61 | 62 | err = ptc.conn.SetWriteDeadline(time.Now().Add(ptc.CommunicationTimeout)) 63 | if err != nil { 64 | return err 65 | } 66 | _, err = ptc.conn.Write(packed) 67 | return err 68 | } 69 | 70 | func (ptc *PlaintextConnection) Read(reader *bufio.Reader) (proto.Message, error) { 71 | // set read lock 72 | ptc.readMutex.Lock() 73 | defer ptc.readMutex.Unlock() 74 | 75 | preamble, err := reader.ReadByte() 76 | if err != nil { 77 | return nil, err 78 | } 79 | if preamble != 0x00 { 80 | if preamble == 0x01 { 81 | return nil, types.ErrConnRequireEncryption 82 | } 83 | return nil, errors.New("esphome_api: invalid preamble. should starts with 0x00 byte") 84 | } 85 | 86 | // get message bytes length 87 | messageSize, err := binary.ReadUvarint(reader) 88 | if err != nil { 89 | return nil, err 90 | } 91 | 92 | // get message type id 93 | messageTypeID, err := binary.ReadUvarint(reader) 94 | if err != nil { 95 | return nil, err 96 | } 97 | 98 | // get message bytes 99 | messageBytes := make([]byte, messageSize) 100 | _, err = io.ReadFull(reader, messageBytes) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | message := api.NewMessageByTypeID(messageTypeID) 106 | if message == nil { 107 | return nil, fmt.Errorf("esphome_api: protocol error: unknown message type %#x", messageTypeID) 108 | } 109 | 110 | err = proto.Unmarshal(messageBytes, message) 111 | if err != nil { 112 | return nil, err 113 | } 114 | return message, nil 115 | } 116 | -------------------------------------------------------------------------------- /cli/command/root/login.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | cliTY "github.com/mycontroller-org/esphome_api/cli/types" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ( 12 | devicePassword string 13 | deviceEncryptionKey string 14 | deviceTimeout time.Duration 15 | ) 16 | 17 | func init() { 18 | AddCommand(loginCmd) 19 | loginCmd.Flags().StringVar(&devicePassword, "password", "", "Password to login into esphome device") 20 | loginCmd.Flags().StringVar(&deviceEncryptionKey, "encryption-key", "", "Encryption key to login into esphome device") 21 | loginCmd.Flags().DurationVar(&deviceTimeout, "timeout", 10*time.Second, "esphome device communication timeout") 22 | 23 | AddCommand(logoutCmd) 24 | } 25 | 26 | var loginCmd = &cobra.Command{ 27 | Use: "login", 28 | Short: "Log in to a esphome device", 29 | Example: ` # login to esphome device without password and encryption key 30 | esphomectl login my_esphome.local:6053 31 | 32 | # login to esphome device with password 33 | esphomectl login my_esphome.local:6053 --password my_secret 34 | 35 | # login to esphome device with encryption key 36 | esphomectl login my_esphome.local:6053 --encryption-key my_encryption_key 37 | `, 38 | Args: cobra.ExactArgs(1), 39 | Run: func(cmd *cobra.Command, args []string) { 40 | deviceCfg := &cliTY.DeviceConfig{ 41 | Address: args[0], 42 | Password: devicePassword, 43 | EncryptionKey: deviceEncryptionKey, 44 | Timeout: deviceTimeout, 45 | } 46 | 47 | _client, err := GetClient(deviceCfg, nil) 48 | if err != nil { 49 | fmt.Fprintln(cmd.ErrOrStderr(), "error on login", err) 50 | return 51 | } 52 | if _client != nil { 53 | deviceInfo, err := _client.DeviceInfo() 54 | if err != nil { 55 | fmt.Fprintln(cmd.ErrOrStderr(), "error on getting device information", err) 56 | return 57 | } 58 | // update device info 59 | deviceCfg.Info = cliTY.DeviceInfo{ 60 | Name: deviceInfo.Name, 61 | Model: deviceInfo.Model, 62 | MacAddress: deviceInfo.MacAddress, 63 | EsphomeVersion: deviceInfo.EsphomeVersion, 64 | CompilationTime: deviceInfo.CompilationTime, 65 | UsesPassword: deviceInfo.UsesPassword, 66 | HasDeepSleep: deviceInfo.HasDeepSleep, 67 | StatusOn: time.Now(), 68 | } 69 | AddDevice(deviceCfg) 70 | WriteConfigFile() 71 | 72 | fmt.Fprintln(cmd.OutOrStdout(), "Login successful.") 73 | fmt.Fprintf(cmd.OutOrStdout(), "%+v\n", deviceInfo) 74 | } 75 | }, 76 | } 77 | 78 | var logoutCmd = &cobra.Command{ 79 | Use: "logout", 80 | Short: "Log out from a esphome device", 81 | Example: ` # logout from a esphome device 82 | esphomectl logout 83 | 84 | # logout from esphome devices 85 | esphomectl logout my_device_1:6053 my_device_2:6053`, 86 | Run: func(cmd *cobra.Command, args []string) { 87 | if len(args) == 0 && CONFIG.Active == "" { 88 | fmt.Fprintln(cmd.ErrOrStderr(), "There is no active device information.") 89 | return 90 | } 91 | 92 | // remove given devices 93 | for _, address := range args { 94 | RemoveDevice(address) 95 | } 96 | WriteConfigFile() 97 | 98 | fmt.Fprintln(cmd.OutOrStdout(), "Logout successful.") 99 | }, 100 | } 101 | -------------------------------------------------------------------------------- /cli/command/device/get_entities.go: -------------------------------------------------------------------------------- 1 | package device 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | rootCmd "github.com/mycontroller-org/esphome_api/cli/command/root" 9 | "github.com/mycontroller-org/esphome_api/pkg/api" 10 | "github.com/mycontroller-org/server/v2/pkg/utils/convertor" 11 | filterUtils "github.com/mycontroller-org/server/v2/pkg/utils/filter_sort" 12 | "github.com/mycontroller-org/server/v2/pkg/utils/printer" 13 | "github.com/spf13/cobra" 14 | "google.golang.org/protobuf/proto" 15 | ) 16 | 17 | var ( 18 | entitiesTimeout time.Duration 19 | ) 20 | 21 | func init() { 22 | getCmd.AddCommand(getEntitiesCmd) 23 | getEntitiesCmd.Flags().DurationVar(&entitiesTimeout, "timeout", 5*time.Second, "Timeout to wait for entities") 24 | } 25 | 26 | var getEntitiesCmd = &cobra.Command{ 27 | Use: "entity", 28 | Short: "Lists available entities", 29 | Aliases: []string{"entities"}, 30 | Example: ` # lists available entities 31 | esphomectl get entities 32 | 33 | # list entities with a timeout 34 | esphomectl get entities --timeout 10s 35 | `, 36 | Run: func(cmd *cobra.Command, args []string) { 37 | 38 | entitiesCollectionDone := false 39 | entities := map[string][]interface{}{} 40 | collectEntities := func(msg proto.Message) { 41 | switch entity := msg.(type) { 42 | case *api.ListEntitiesDoneResponse: 43 | entitiesCollectionDone = true 44 | 45 | default: 46 | _, _deviceClass, err := filterUtils.GetValueByKeyPath(entity, "deviceClass") 47 | if err != nil { 48 | fmt.Fprintln(cmd.ErrOrStderr(), "error:", err) 49 | return 50 | } 51 | deviceClass := convertor.ToString(_deviceClass) 52 | _sensors, found := entities[deviceClass] 53 | if !found { 54 | _sensors = make([]interface{}, 0) 55 | } 56 | _sensors = append(_sensors, entity) 57 | entities[deviceClass] = _sensors 58 | } 59 | } 60 | 61 | client, err := rootCmd.GetActiveClient(collectEntities) 62 | if err != nil { 63 | fmt.Fprintln(cmd.ErrOrStderr(), "error:", err.Error()) 64 | return 65 | } 66 | 67 | err = client.ListEntities() 68 | if err != nil { 69 | fmt.Fprintln(cmd.ErrOrStderr(), "error:", err.Error()) 70 | return 71 | } 72 | 73 | ticker := time.NewTicker(200 * time.Millisecond) 74 | timeoutTime := time.Now().Add(entitiesTimeout) 75 | for range ticker.C { 76 | if entitiesCollectionDone || time.Now().Before(timeoutTime) { 77 | break 78 | } 79 | } 80 | 81 | if len(entities) == 0 { 82 | fmt.Fprintln(cmd.OutOrStdout(), "No resource found") 83 | return 84 | } 85 | 86 | for k, _sensors := range entities { 87 | fmt.Fprintln(cmd.OutOrStdout()) 88 | fmt.Fprintln(cmd.OutOrStdout(), strings.ToUpper(k)) 89 | 90 | switch k { 91 | case "light": 92 | headers := []printer.Header{ 93 | {Title: "name", ValuePath: "name"}, 94 | {Title: "object id", ValuePath: "objectId"}, 95 | {Title: "key", ValuePath: "key"}, 96 | {Title: "unique id", ValuePath: "uniqueId"}, 97 | {Title: "effects", ValuePath: "effects"}, 98 | {Title: "icon", ValuePath: "icon"}, 99 | } 100 | printer.Print(cmd.OutOrStdout(), headers, _sensors, rootCmd.HideHeader, rootCmd.OutputFormat, rootCmd.Pretty) 101 | 102 | default: 103 | headers := []printer.Header{ 104 | {Title: "name", ValuePath: "name"}, 105 | {Title: "object id", ValuePath: "objectId"}, 106 | {Title: "key", ValuePath: "key"}, 107 | {Title: "unique id", ValuePath: "uniqueId"}, 108 | {Title: "device class", ValuePath: "deviceClass"}, 109 | } 110 | printer.Print(cmd.OutOrStdout(), headers, _sensors, rootCmd.HideHeader, rootCmd.OutputFormat, rootCmd.Pretty) 111 | 112 | } 113 | 114 | } 115 | 116 | }, 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esphome_api 2 | 3 | A Go library to manage [ESPHome](https://esphome.io/) devices. 4 | 5 | This Go library provides a client implementation for interacting with ESPHome 6 | devices using the native ESPHome API. It enables developers to control and 7 | monitor ESPHome devices programmatically from Go applications. The library 8 | offers functionalities for establishing connections, authenticating, sending 9 | commands, subscribing to state updates, and receiving device information. 10 | 11 | ## Installation 12 | 13 | To install the `esphome_api` library, use the following command: 14 | 15 | ```bash 16 | go build -o esphome ./cli/main.go 17 | ``` 18 | 19 | ## Usage 20 | 21 | ### 1. Import the Library 22 | 23 | ```go 24 | import ( 25 | "fmt" 26 | "log" 27 | "time" 28 | 29 | "github.com/mycontroller-org/esphome_api/pkg/api" 30 | "github.com/mycontroller-org/esphome_api/pkg/client" 31 | "google.golang.org/protobuf/proto" 32 | ) 33 | ``` 34 | 35 | ### 2. Create a Client 36 | 37 | ```go 38 | // Replace with your ESPHome device's address and encryption key (if applicable) 39 | address := "esphome.local:6053" 40 | encryptionKey := "YOUR_ENCRYPTION_KEY" 41 | 42 | // Create a new ESPHome API client 43 | client, err := client.GetClient("my-client-id", address, encryptionKey, 10*time.Second, handleFunc) 44 | if err != nil { 45 | log.Fatalln(err) 46 | } 47 | defer client.Close() 48 | ``` 49 | 50 | ### 3. Connect and Authenticate (if required) 51 | 52 | ```go 53 | // If your device requires authentication, log in with the password 54 | password := "YOUR_PASSWORD" 55 | if err := client.Login(password); err != nil { 56 | log.Fatalln(err) 57 | } 58 | ``` 59 | 60 | ### 4. Send Commands and Receive State Updates 61 | 62 | ```go 63 | // Example: Subscribe to state updates 64 | if err := client.SubscribeStates(); err != nil { 65 | log.Fatalln(err) 66 | } 67 | 68 | // Example: Send a command to a light entity 69 | lightKey := uint32(12) // Replace with the key of your light entity 70 | if err := client.Send(&api.LightCommandRequest{ 71 | Key: lightKey, 72 | State: true, // Turn the light on 73 | }); err != nil { 74 | log.Fatalln(err) 75 | } 76 | ``` 77 | 78 | ### 5. Handle Incoming Messages 79 | 80 | ```go 81 | // Define a handler function to process incoming messages 82 | func handleFunc(msg proto.Message) { 83 | switch msg := msg.(type) { 84 | case *api.LightStateResponse: 85 | fmt.Printf("Light state update: Key=%d, State=%t\n", msg.Key, msg.State) 86 | 87 | case *api.BinarySensorStateResponse: 88 | fmt.Printf("Binary sensor state update: Key=%d, State=%t\n", msg.Key, msg.State) 89 | 90 | // Handle other message types as needed 91 | default: 92 | fmt.Printf("Received message of type: %T\n", msg) 93 | } 94 | } 95 | ``` 96 | 97 | ## Configuration Example 98 | 99 | The following is a complete configuration example for the `esphomectl` CLI 100 | tool, which utilizes the `esphome_api` library: 101 | 102 | **File: `~/.esphomectl.yaml`** 103 | 104 | ```yaml 105 | active: esphome.local:6053 106 | devices: 107 | - address: esphome.local:6053 108 | password: BASE64/YOUR_ENCODED_PASSWORD 109 | encryptionKey: YOUR_ENCRYPTION_KEY 110 | timeout: 10s 111 | info: 112 | name: My ESPHome Device 113 | model: NodeMCU 114 | macAddress: AC:BC:32:89:0E:A9 115 | esphomeVersion: "1.15.0" 116 | compilationTime: "2023-10-26T10:00:00" 117 | usesPassword: true 118 | hasDeepSleep: false 119 | statusOn: 2023-10-26T12:00:00+05:30 120 | ``` 121 | 122 | **Note:** The password is encoded in Base64 format. You can encode your password using the following command: 123 | 124 | ```bash 125 | echo -n "YOUR_PASSWORD" | base64 126 | ``` 127 | 128 | ## Examples 129 | 130 | The [examples](/examples/) directory contains various examples demonstrating the usage of the `esphome_api` library for different purposes, such as: 131 | 132 | - **camera:** Capture images from an ESPHome camera. 133 | - **device_info:** Retrieve device information from an ESPHome device. 134 | - **tail_logs:** Subscribe to and display logs from an ESPHome device. 135 | -------------------------------------------------------------------------------- /cli/command/root/cmd.go: -------------------------------------------------------------------------------- 1 | package root 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | homedir "github.com/mitchellh/go-homedir" 10 | cliTY "github.com/mycontroller-org/esphome_api/cli/types" 11 | "github.com/mycontroller-org/esphome_api/pkg/client" 12 | TY "github.com/mycontroller-org/esphome_api/pkg/types" 13 | clientTY "github.com/mycontroller-org/server/v2/pkg/types/client" 14 | printer "github.com/mycontroller-org/server/v2/pkg/utils/printer" 15 | "gopkg.in/yaml.v3" 16 | 17 | "github.com/spf13/cobra" 18 | "github.com/spf13/viper" 19 | ) 20 | 21 | const ( 22 | ENV_PREFIX = "ESPHOME" 23 | CONFIG_FILE_NAME = ".esphomectl" 24 | CONFIG_FILE_EXT = "yaml" 25 | CONFIG_FILE_ENV = "ESPHOMECTL_CONFIG" 26 | ) 27 | 28 | var ( 29 | cfgFile string 30 | CONFIG *cliTY.Config // keeps device details 31 | ioStreams clientTY.IOStreams // read and write to this stream 32 | 33 | HideHeader bool 34 | Pretty bool 35 | OutputFormat string 36 | 37 | rootCliLong = `esphome cli Client 38 | 39 | This client helps you to control your esphome devices from the command line. 40 | ` 41 | ) 42 | 43 | func AddCommand(cmds ...*cobra.Command) { 44 | rootCmd.AddCommand(cmds...) 45 | } 46 | 47 | var rootCmd = &cobra.Command{ 48 | Use: "esphomectl", 49 | Short: "esphomectl", 50 | Long: rootCliLong, 51 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 52 | cmd.SetOut(ioStreams.Out) 53 | cmd.SetErr(ioStreams.ErrOut) 54 | }, 55 | } 56 | 57 | func init() { 58 | CONFIG = &cliTY.Config{} 59 | 60 | cobra.OnInitialize(loadConfig) 61 | 62 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.esphomectl.yaml)") 63 | rootCmd.PersistentFlags().StringVarP(&OutputFormat, "output", "o", printer.OutputConsole, "output format. options: yaml, json, console, wide") 64 | rootCmd.PersistentFlags().BoolVar(&HideHeader, "hide-header", false, "hides the header on the console output") 65 | rootCmd.PersistentFlags().BoolVar(&Pretty, "pretty", false, "JSON pretty print") 66 | } 67 | 68 | func GetActiveClient(callBackFunc TY.CallBackFunc) (*client.Client, error) { 69 | if CONFIG.Active == "" { 70 | return nil, errors.New("no device configured") 71 | } 72 | cfg := GetDevice(CONFIG.Active) 73 | if cfg == nil { 74 | return nil, fmt.Errorf("device[%s] configuration is not available", CONFIG.Active) 75 | } 76 | return GetClient(cfg, callBackFunc) 77 | } 78 | 79 | func GetClient(cfg *cliTY.DeviceConfig, callBackFunc TY.CallBackFunc) (*client.Client, error) { 80 | _client, err := client.GetClient("mc_esphome_cli", cfg.Address, cfg.EncryptionKey, cfg.Timeout, callBackFunc) 81 | if err != nil { 82 | return nil, err 83 | } 84 | if cfg.GetPassword() != "" { 85 | err = _client.Login(cfg.GetPassword()) 86 | if err != nil { 87 | return nil, err 88 | } 89 | } 90 | 91 | return _client, nil 92 | } 93 | 94 | func Execute(streams clientTY.IOStreams) { 95 | ioStreams = streams 96 | if err := rootCmd.Execute(); err != nil { 97 | fmt.Fprintln(ioStreams.ErrOut, err) 98 | os.Exit(1) 99 | } 100 | } 101 | 102 | func WriteConfigFile() { 103 | if cfgFile == "" { 104 | return 105 | } 106 | if CONFIG == nil { 107 | CONFIG = &cliTY.Config{} 108 | } 109 | 110 | configBytes, err := yaml.Marshal(CONFIG) 111 | if err != nil { 112 | fmt.Fprintf(ioStreams.ErrOut, "error on config file marshal. error:[%s]\n", err.Error()) 113 | } 114 | err = os.WriteFile(cfgFile, configBytes, os.ModePerm) 115 | if err != nil { 116 | fmt.Fprintf(ioStreams.ErrOut, "error on writing config file to disk, filename:%s, error:[%s]\n", cfgFile, err.Error()) 117 | } 118 | } 119 | 120 | func loadConfig() { 121 | if cfgFile != "" { 122 | // Use config file from the flag. 123 | viper.SetConfigFile(cfgFile) 124 | } else if os.Getenv(CONFIG_FILE_ENV) != "" { 125 | cfgFile = os.Getenv(CONFIG_FILE_ENV) 126 | } else { 127 | // Find home directory.initConfig 128 | home, err := homedir.Dir() 129 | cobra.CheckErr(err) 130 | 131 | // Search config in home directory with name ".myc" (without extension). 132 | viper.AddConfigPath(home) 133 | viper.SetConfigName(CONFIG_FILE_NAME) 134 | viper.SetConfigType(CONFIG_FILE_EXT) 135 | 136 | cfgFile = filepath.Join(home, fmt.Sprintf("%s.%s", CONFIG_FILE_NAME, CONFIG_FILE_EXT)) 137 | } 138 | 139 | viper.SetEnvPrefix(ENV_PREFIX) 140 | viper.AutomaticEnv() 141 | 142 | if err := viper.ReadInConfig(); err == nil { 143 | err = viper.Unmarshal(&CONFIG) 144 | if err != nil { 145 | fmt.Fprint(ioStreams.ErrOut, "error on unmarshal of config\n", err) 146 | } 147 | } 148 | } 149 | 150 | func GetDevice(address string) *cliTY.DeviceConfig { 151 | for _, device := range CONFIG.Devices { 152 | if device.Address == address { 153 | _device := device.Clone() 154 | return &_device 155 | } 156 | } 157 | return nil 158 | } 159 | 160 | func RemoveDevice(address string) { 161 | devices := make([]cliTY.DeviceConfig, 0) 162 | for _, device := range CONFIG.Devices { 163 | if device.Address == address { 164 | continue 165 | } 166 | devices = append(devices, device) 167 | } 168 | CONFIG.Devices = devices 169 | CONFIG.Active = "" 170 | } 171 | 172 | func AddDevice(newDevice *cliTY.DeviceConfig) { 173 | newDevice.EncodePassword() 174 | devices := make([]cliTY.DeviceConfig, 0) 175 | for _, device := range CONFIG.Devices { 176 | if device.Address == newDevice.Address { 177 | continue 178 | } 179 | devices = append(devices, device) 180 | } 181 | 182 | // add current device 183 | devices = append(devices, *newDevice) 184 | CONFIG.Devices = devices 185 | CONFIG.Active = newDevice.Address 186 | } 187 | -------------------------------------------------------------------------------- /pkg/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | "google.golang.org/protobuf/proto" 11 | 12 | "github.com/mycontroller-org/esphome_api/pkg/api" 13 | "github.com/mycontroller-org/esphome_api/pkg/connection" 14 | types "github.com/mycontroller-org/esphome_api/pkg/types" 15 | ) 16 | 17 | // Client struct. 18 | type Client struct { 19 | ID string 20 | conn net.Conn 21 | reader *bufio.Reader 22 | stopChan chan bool 23 | waitMapMutex sync.RWMutex 24 | waitMap map[uint64]chan proto.Message 25 | lastMessageAt time.Time 26 | callBackFunc types.CallBackFunc 27 | CommunicationTimeout time.Duration 28 | apiConn connection.ApiConnection 29 | } 30 | 31 | // GetClient returns esphome api client 32 | func GetClient(clientID, address, encryptionKey string, timeout time.Duration, callBackFunc types.CallBackFunc) (*Client, error) { 33 | conn, err := net.DialTimeout("tcp", address, timeout) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | // add noop func, if handler not defined 39 | if callBackFunc == nil { 40 | callBackFunc = func(msg proto.Message) {} 41 | } 42 | 43 | apiConn, err := connection.GetConnection(conn, timeout, encryptionKey) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | c := &Client{ 49 | ID: clientID, 50 | conn: conn, 51 | reader: bufio.NewReader(conn), 52 | waitMap: make(map[uint64]chan proto.Message), 53 | stopChan: make(chan bool), 54 | callBackFunc: callBackFunc, 55 | CommunicationTimeout: timeout, 56 | apiConn: apiConn, 57 | } 58 | 59 | // call handshake, used in encrypted connection 60 | err = apiConn.Handshake() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | go c.messageReader() 66 | return c, nil 67 | } 68 | 69 | // Close the client 70 | func (c *Client) Close() error { 71 | _, err := c.SendAndWaitForResponse(&api.DisconnectRequest{}, api.DisconnectResponseTypeID) 72 | select { 73 | case c.stopChan <- true: 74 | default: 75 | } 76 | return err 77 | } 78 | 79 | // Hello func 80 | func (c *Client) Hello() (*types.HelloResponse, error) { 81 | response, err := c.SendAndWaitForResponse(&api.HelloRequest{ 82 | ClientInfo: c.ID, 83 | }, api.HelloResponseTypeID) 84 | if err != nil { 85 | return nil, err 86 | } 87 | helloResponse, ok := response.(*api.HelloResponse) 88 | if !ok { 89 | return nil, fmt.Errorf("invalid response type:%T", response) 90 | } 91 | return &types.HelloResponse{ 92 | ApiVersionMajor: helloResponse.ApiVersionMajor, 93 | ApiVersionMinor: helloResponse.ApiVersionMinor, 94 | ServerInfo: helloResponse.ServerInfo, 95 | Name: helloResponse.Name, 96 | }, nil 97 | } 98 | 99 | // Login func 100 | func (c *Client) Login(password string) error { 101 | _, err := c.Hello() 102 | if err != nil { 103 | return err 104 | } 105 | 106 | message, err := c.SendAndWaitForResponse(&api.ConnectRequest{ 107 | Password: password, 108 | }, api.ConnectResponseTypeID) 109 | if err != nil { 110 | return err 111 | } 112 | connectResponse := message.(*api.ConnectResponse) 113 | if connectResponse.InvalidPassword { 114 | return types.ErrPassword 115 | } 116 | 117 | return nil 118 | } 119 | 120 | // Ping func 121 | func (c *Client) Ping() error { 122 | _, err := c.SendAndWaitForResponse(&api.PingRequest{}, api.PingResponseTypeID) 123 | return err 124 | } 125 | 126 | // SubscribeStates func 127 | func (c *Client) SubscribeStates() error { 128 | if err := c.Send(&api.SubscribeStatesRequest{}); err != nil { 129 | return err 130 | } 131 | return nil 132 | } 133 | 134 | // LastMessage returns the time of the last message received. 135 | func (c *Client) LastMessageAt() time.Time { 136 | return c.lastMessageAt 137 | } 138 | 139 | // DeviceInfo queries the ESPHome device information. 140 | func (c *Client) DeviceInfo() (*types.DeviceInfo, error) { 141 | message, err := c.SendAndWaitForResponse(&api.DeviceInfoRequest{}, api.DeviceInfoResponseTypeID) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | info := message.(*api.DeviceInfoResponse) 147 | return &types.DeviceInfo{ 148 | UsesPassword: info.UsesPassword, 149 | Name: info.Name, 150 | MacAddress: info.MacAddress, 151 | EsphomeVersion: info.EsphomeVersion, 152 | CompilationTime: info.CompilationTime, 153 | Model: info.Model, 154 | HasDeepSleep: info.HasDeepSleep, 155 | }, nil 156 | } 157 | 158 | // SubscribeLogs func 159 | func (c *Client) SubscribeLogs(level types.LogLevel) error { 160 | if err := c.Send(&api.SubscribeLogsRequest{ 161 | Level: api.LogLevel(level), 162 | }); err != nil { 163 | return err 164 | } 165 | 166 | return nil 167 | } 168 | 169 | // ListEntities func 170 | func (c *Client) ListEntities() error { 171 | return c.Send(&api.ListEntitiesRequest{}) 172 | } 173 | 174 | // messageReader reads message from the node 175 | func (c *Client) messageReader() { 176 | defer c.conn.Close() 177 | for { 178 | select { 179 | case <-c.stopChan: 180 | return 181 | 182 | default: 183 | if err := c.getMessage(); err != nil { 184 | return 185 | } 186 | } 187 | } 188 | } 189 | 190 | func (c *Client) getMessage() error { 191 | var message proto.Message 192 | message, err := c.apiConn.Read(c.reader) 193 | if err == nil { 194 | c.lastMessageAt = time.Now() 195 | // check waiting map 196 | c.waitMapMutex.Lock() 197 | in, found := c.waitMap[api.TypeID(message)] 198 | c.waitMapMutex.Unlock() 199 | if found { 200 | in <- message 201 | } 202 | 203 | // forward to other parties 204 | if c.handleInternal(message) { 205 | return nil 206 | } else if c.isExternal(message) { 207 | if c.callBackFunc != nil { 208 | c.callBackFunc(message) 209 | return nil 210 | } 211 | } 212 | } 213 | 214 | return err 215 | } 216 | 217 | func (c *Client) isExternal(message proto.Message) bool { 218 | switch message.(type) { 219 | case 220 | *api.PingResponse, 221 | *api.HelloResponse, 222 | *api.ConnectResponse, 223 | *api.DeviceInfoResponse, 224 | *api.DisconnectResponse: 225 | return false 226 | } 227 | return true 228 | } 229 | 230 | func (c *Client) handleInternal(message proto.Message) bool { 231 | switch message.(type) { 232 | case *api.DisconnectRequest: 233 | _ = c.Send(&api.DisconnectResponse{}) 234 | c.Close() 235 | return true 236 | 237 | case *api.PingRequest: 238 | _ = c.Send(&api.PingResponse{}) 239 | return true 240 | 241 | case *api.HelloRequest: 242 | _ = c.Send(&api.HelloResponse{}) 243 | return true 244 | 245 | case *api.ConnectRequest: 246 | _ = c.Send(&api.ConnectResponse{}) 247 | return true 248 | 249 | } 250 | 251 | return false 252 | } 253 | 254 | func (c *Client) Send(message proto.Message) error { 255 | return c.apiConn.Write(message) 256 | } 257 | 258 | func (c *Client) SendAndWaitForResponse(message proto.Message, messageType uint64) (proto.Message, error) { 259 | if err := c.Send(message); err != nil { 260 | return nil, err 261 | } 262 | return c.waitForMessage(messageType) 263 | } 264 | 265 | func (c *Client) waitForMessage(messageType uint64) (proto.Message, error) { 266 | in := make(chan proto.Message, 1) 267 | c.waitFor(messageType, in) 268 | defer c.waitDone(messageType) 269 | 270 | select { 271 | case message := <-in: 272 | return message, nil 273 | case <-time.After(c.CommunicationTimeout): 274 | return nil, types.ErrCommunicationTimeout 275 | } 276 | } 277 | 278 | func (c *Client) waitFor(messageType uint64, in chan proto.Message) { 279 | c.waitMapMutex.Lock() 280 | defer c.waitMapMutex.Unlock() 281 | 282 | other, waiting := c.waitMap[messageType] 283 | if waiting { 284 | other <- nil 285 | close(other) 286 | } 287 | c.waitMap[messageType] = in 288 | } 289 | 290 | func (c *Client) waitDone(messageType uint64) { 291 | c.waitMapMutex.Lock() 292 | defer c.waitMapMutex.Unlock() 293 | delete(c.waitMap, messageType) 294 | } 295 | -------------------------------------------------------------------------------- /pkg/connection/conn_encryption.go: -------------------------------------------------------------------------------- 1 | package connection 2 | 3 | import ( 4 | "bufio" 5 | "encoding/base64" 6 | "encoding/binary" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "net" 11 | "sync" 12 | "time" 13 | 14 | "github.com/flynn/noise" 15 | "github.com/mycontroller-org/esphome_api/pkg/api" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | var ( 20 | ErrHandshakeFailed = errors.New("esphome_api: handshake failed") 21 | ErrHandshakeNotDone = errors.New("esphome_api: handshake not done") 22 | ErrInvalidEncryptionPreamble = errors.New("esphome_api: invalid preamble. encrypted message should starts with 0x01") 23 | ) 24 | 25 | // encryption is based on noise protocol 26 | // reference: https://noiseprotocol.org 27 | type EncryptedConnection struct { 28 | conn net.Conn 29 | CommunicationTimeout time.Duration 30 | encryptionKey []byte 31 | handshakeState *noise.HandshakeState 32 | isHandshakeSucceed bool 33 | handshakeResponse []byte 34 | encryption *noise.CipherState 35 | decryption *noise.CipherState 36 | writeMutex sync.Mutex 37 | readMutex sync.Mutex 38 | } 39 | 40 | func NewEncryptedConnection(conn net.Conn, communicationTimeout time.Duration, encryptionKey string) (ApiConnection, error) { 41 | // decode base64 key 42 | _psk, err := base64.StdEncoding.DecodeString(encryptionKey) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | if len(_psk) != 32 { 48 | return nil, fmt.Errorf("esphome_api: encryption key size should be 32 bytes. received:%d", len(_psk)) 49 | } 50 | 51 | ec := &EncryptedConnection{ 52 | conn: conn, 53 | CommunicationTimeout: communicationTimeout, 54 | encryptionKey: _psk, 55 | isHandshakeSucceed: false, 56 | } 57 | return ec, nil 58 | } 59 | 60 | // handshake implemented based on python implementation 61 | // reference: https://github.com/esphome/aioesphomeapi/blob/main/aioesphomeapi/_frame_helper.py#L250 62 | func (ec *EncryptedConnection) Handshake() error { 63 | // send empty message 64 | err := ec.writeToTransport(nil) 65 | if err != nil { 66 | return err 67 | } 68 | // wait for a response 69 | err = ec.waitForResponse() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | // create handshake state 75 | _hs, err := noise.NewHandshakeState( 76 | noise.Config{ 77 | Pattern: noise.HandshakeNN, 78 | CipherSuite: noise.NewCipherSuite(noise.DH25519, noise.CipherChaChaPoly, noise.HashSHA256), 79 | Initiator: true, 80 | Prologue: []byte("NoiseAPIInit" + "\x00\x00"), 81 | PresharedKey: ec.encryptionKey, 82 | PresharedKeyPlacement: 0, 83 | }, 84 | ) 85 | if err != nil { 86 | return err 87 | } 88 | ec.handshakeState = _hs 89 | 90 | // create handshake message 91 | handshakeMessage, _, _, err := ec.handshakeState.WriteMessage(nil, nil) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | // append 0x00 in front of handshake message 97 | handshakeMessage = append([]byte{0x00}, handshakeMessage...) 98 | 99 | // send handshake message to the transport 100 | err = ec.writeToTransport(handshakeMessage) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | // wait for handshake response 106 | // handshake response will be added in ec.handshakeResponse 107 | err = ec.waitForResponse() 108 | if err != nil { 109 | return err 110 | } 111 | 112 | // if handshake response available, process it and update encrypt and decrypt cipherState 113 | if len(ec.handshakeResponse) > 0 { 114 | // discard first byte of handshake response 115 | _, _encryptCipherState, _decryptCipherState, err := ec.handshakeState.ReadMessage(nil, ec.handshakeResponse[1:]) 116 | if err == nil { 117 | ec.encryption = _encryptCipherState 118 | ec.decryption = _decryptCipherState 119 | ec.isHandshakeSucceed = true 120 | } else { 121 | return err 122 | } 123 | } 124 | 125 | // return error if handshake not succeed 126 | if !ec.isHandshakeSucceed { 127 | return ErrHandshakeFailed 128 | } 129 | 130 | return nil 131 | } 132 | 133 | func (ec *EncryptedConnection) waitForResponse() error { 134 | _reader := bufio.NewReader(ec.conn) 135 | _, err := ec.Read(_reader) 136 | // ignore handshake not done error 137 | if err != nil && !errors.Is(err, ErrHandshakeNotDone) { 138 | return err 139 | } 140 | return nil 141 | } 142 | 143 | func (ec *EncryptedConnection) writeToTransport(data []byte) error { 144 | if data == nil { 145 | data = []byte{} 146 | } 147 | 148 | // preamble + data length (2 bytes fixed) 149 | header := make([]byte, 3) 150 | 151 | // preamble 152 | header[0] = 0x01 153 | 154 | // message length 155 | header[1] = byte(len(data) >> 8 & 0xff) 156 | header[2] = byte(len(data) & 0xff) 157 | 158 | // append header + data 159 | packed := append(header, data...) 160 | 161 | // set write lock 162 | ec.writeMutex.Lock() 163 | defer ec.writeMutex.Unlock() 164 | 165 | err := ec.conn.SetWriteDeadline(time.Now().Add(ec.CommunicationTimeout)) 166 | if err != nil { 167 | return err 168 | } 169 | _, err = ec.conn.Write(packed) 170 | return err 171 | } 172 | 173 | func (ec *EncryptedConnection) Write(message proto.Message) error { 174 | messageBytes, err := proto.Marshal(message) 175 | if err != nil { 176 | return err 177 | } 178 | 179 | // type id (2 bytes) + message length (2 bytes) 180 | header := make([]byte, 4) 181 | 182 | typeID := api.TypeID(message) 183 | header[0] = byte(typeID >> 8 & 0xff) 184 | header[1] = byte(typeID & 0xff) 185 | 186 | header[2] = byte(len(messageBytes) >> 8 & 0xff) 187 | header[3] = byte(len(messageBytes) & 0xff) 188 | 189 | packed := append(header, messageBytes...) 190 | 191 | // encrypt the data 192 | encryptedData, err := ec.encrypt(packed) 193 | if err != nil { 194 | return err 195 | } 196 | return ec.writeToTransport(encryptedData) 197 | } 198 | 199 | func (ec *EncryptedConnection) Read(reader *bufio.Reader) (proto.Message, error) { 200 | // set read lock 201 | ec.readMutex.Lock() 202 | defer ec.readMutex.Unlock() 203 | 204 | header := make([]byte, 3) // limit to 3 bytes of header 205 | _, err := io.ReadFull(reader, header) 206 | if err != nil { 207 | return nil, err 208 | } 209 | 210 | preamble := header[0] 211 | if preamble != 0x01 { 212 | return nil, ErrInvalidEncryptionPreamble 213 | } 214 | 215 | encryptedMessageSize := binary.BigEndian.Uint16(header[1:3]) 216 | 217 | // get encrypted message bytes 218 | encryptedMessageBytes := make([]byte, encryptedMessageSize) 219 | _, err = io.ReadFull(reader, encryptedMessageBytes) 220 | if err != nil { 221 | return nil, err 222 | } 223 | 224 | // if handshake is not done, these bytes may be a handshake response 225 | if !ec.isHandshakeSucceed { 226 | ec.handshakeResponse = encryptedMessageBytes 227 | } 228 | 229 | // if there is no bytes to decrypt, return from here 230 | if len(encryptedMessageBytes) == 0 { 231 | return nil, nil 232 | } 233 | 234 | // decrypt the message 235 | decryptedMessageBytes, err := ec.decrypt(encryptedMessageBytes) 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | // get type id and message size 241 | messageTypeID := uint64(binary.BigEndian.Uint16(decryptedMessageBytes[0:2])) 242 | messageSize := int(binary.BigEndian.Uint16(decryptedMessageBytes[2:4])) 243 | 244 | messageBytes := decryptedMessageBytes[4:] 245 | 246 | if len(messageBytes) != messageSize { 247 | return nil, fmt.Errorf("esphome_api: message length mismatched. expected:%d, actual:%d", messageSize, len(messageBytes)) 248 | } 249 | 250 | message := api.NewMessageByTypeID(messageTypeID) 251 | if message == nil { 252 | return nil, fmt.Errorf("esphome_api: protocol error: unknown message type %#x", messageTypeID) 253 | } 254 | 255 | err = proto.Unmarshal(messageBytes, message) 256 | if err != nil { 257 | return nil, err 258 | } 259 | 260 | return message, nil 261 | } 262 | 263 | func (ec *EncryptedConnection) encrypt(data []byte) ([]byte, error) { 264 | if ec.isHandshakeSucceed { 265 | return ec.encryption.Encrypt(nil, nil, data) 266 | } 267 | return nil, ErrHandshakeNotDone 268 | } 269 | 270 | func (ec *EncryptedConnection) decrypt(data []byte) ([]byte, error) { 271 | if ec.isHandshakeSucceed { 272 | return ec.decryption.Decrypt(nil, nil, data) 273 | } 274 | return nil, ErrHandshakeNotDone 275 | } 276 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/flynn/noise v1.0.1-0.20220214164934-d803f5c4b0f4 h1:6pcIWmKkQZdpPjs/pD9OLt0NwftBozNE0Nm5zMCG2C4= 7 | github.com/flynn/noise v1.0.1-0.20220214164934-d803f5c4b0f4/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= 8 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 9 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 10 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 11 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 12 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 13 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 14 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 15 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 16 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 17 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 18 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 19 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 22 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 23 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 24 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 25 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 26 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 27 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 28 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 29 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 30 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 31 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= 32 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 33 | github.com/mycontroller-org/server/v2 v2.0.1-0.20240615005612-617ab65354e9 h1:vdDJn/Xn0zCG4rDwtdvU9NTGfqqDUvCcTiutObGsTT4= 34 | github.com/mycontroller-org/server/v2 v2.0.1-0.20240615005612-617ab65354e9/go.mod h1:tWt8BkFKFNOVjm6Yak+RYmHknaRtBFaQwlI09N0q84U= 35 | github.com/nleeper/goment v1.4.4 h1:GlMTpxvhueljArSunzYjN9Ri4SOmpn0Vh2hg2z/IIl8= 36 | github.com/nleeper/goment v1.4.4/go.mod h1:zDl5bAyDhqxwQKAvkSXMRLOdCowrdZz53ofRJc4VhTo= 37 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 38 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 39 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 40 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 43 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 44 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 45 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 46 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 47 | github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= 48 | github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 49 | github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 50 | github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 51 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 52 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 53 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 54 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 55 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 56 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 57 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 58 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 59 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 60 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 61 | github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 62 | github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 63 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 64 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 65 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 66 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 67 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 68 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 69 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 70 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 71 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 72 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 73 | github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 74 | github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 75 | github.com/tkuchiki/go-timezone v0.2.0 h1:yyZVHtQRVZ+wvlte5HXvSpBkR0dPYnPEIgq9qqAqltk= 76 | github.com/tkuchiki/go-timezone v0.2.0/go.mod h1:b1Ean9v2UXtxSq4TZF0i/TU9NuoWa9hOzOKoGCV2zqY= 77 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 78 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 79 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 80 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 81 | go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= 82 | go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 83 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 84 | golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= 85 | golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 86 | golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= 87 | golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= 88 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 89 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 90 | golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 91 | golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 92 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 93 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 94 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 95 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 96 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 97 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 98 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 99 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 100 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 101 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 102 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 103 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 104 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 105 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 106 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 107 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 20202 Jeeva Kandasamy 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /pkg/api/api_options.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.28.0 4 | // protoc v3.21.12 5 | // source: api_options.proto 6 | 7 | package api 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | descriptorpb "google.golang.org/protobuf/types/descriptorpb" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type APISourceType int32 25 | 26 | const ( 27 | APISourceType_SOURCE_BOTH APISourceType = 0 28 | APISourceType_SOURCE_SERVER APISourceType = 1 29 | APISourceType_SOURCE_CLIENT APISourceType = 2 30 | ) 31 | 32 | // Enum value maps for APISourceType. 33 | var ( 34 | APISourceType_name = map[int32]string{ 35 | 0: "SOURCE_BOTH", 36 | 1: "SOURCE_SERVER", 37 | 2: "SOURCE_CLIENT", 38 | } 39 | APISourceType_value = map[string]int32{ 40 | "SOURCE_BOTH": 0, 41 | "SOURCE_SERVER": 1, 42 | "SOURCE_CLIENT": 2, 43 | } 44 | ) 45 | 46 | func (x APISourceType) Enum() *APISourceType { 47 | p := new(APISourceType) 48 | *p = x 49 | return p 50 | } 51 | 52 | func (x APISourceType) String() string { 53 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 54 | } 55 | 56 | func (APISourceType) Descriptor() protoreflect.EnumDescriptor { 57 | return file_api_options_proto_enumTypes[0].Descriptor() 58 | } 59 | 60 | func (APISourceType) Type() protoreflect.EnumType { 61 | return &file_api_options_proto_enumTypes[0] 62 | } 63 | 64 | func (x APISourceType) Number() protoreflect.EnumNumber { 65 | return protoreflect.EnumNumber(x) 66 | } 67 | 68 | // Deprecated: Do not use. 69 | func (x *APISourceType) UnmarshalJSON(b []byte) error { 70 | num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b) 71 | if err != nil { 72 | return err 73 | } 74 | *x = APISourceType(num) 75 | return nil 76 | } 77 | 78 | // Deprecated: Use APISourceType.Descriptor instead. 79 | func (APISourceType) EnumDescriptor() ([]byte, []int) { 80 | return file_api_options_proto_rawDescGZIP(), []int{0} 81 | } 82 | 83 | type Void struct { 84 | state protoimpl.MessageState 85 | sizeCache protoimpl.SizeCache 86 | unknownFields protoimpl.UnknownFields 87 | } 88 | 89 | func (x *Void) Reset() { 90 | *x = Void{} 91 | if protoimpl.UnsafeEnabled { 92 | mi := &file_api_options_proto_msgTypes[0] 93 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 94 | ms.StoreMessageInfo(mi) 95 | } 96 | } 97 | 98 | func (x *Void) String() string { 99 | return protoimpl.X.MessageStringOf(x) 100 | } 101 | 102 | func (*Void) ProtoMessage() {} 103 | 104 | func (x *Void) ProtoReflect() protoreflect.Message { 105 | mi := &file_api_options_proto_msgTypes[0] 106 | if protoimpl.UnsafeEnabled && x != nil { 107 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 108 | if ms.LoadMessageInfo() == nil { 109 | ms.StoreMessageInfo(mi) 110 | } 111 | return ms 112 | } 113 | return mi.MessageOf(x) 114 | } 115 | 116 | // Deprecated: Use Void.ProtoReflect.Descriptor instead. 117 | func (*Void) Descriptor() ([]byte, []int) { 118 | return file_api_options_proto_rawDescGZIP(), []int{0} 119 | } 120 | 121 | var file_api_options_proto_extTypes = []protoimpl.ExtensionInfo{ 122 | { 123 | ExtendedType: (*descriptorpb.MethodOptions)(nil), 124 | ExtensionType: (*bool)(nil), 125 | Field: 1038, 126 | Name: "needs_setup_connection", 127 | Tag: "varint,1038,opt,name=needs_setup_connection,def=1", 128 | Filename: "api_options.proto", 129 | }, 130 | { 131 | ExtendedType: (*descriptorpb.MethodOptions)(nil), 132 | ExtensionType: (*bool)(nil), 133 | Field: 1039, 134 | Name: "needs_authentication", 135 | Tag: "varint,1039,opt,name=needs_authentication,def=1", 136 | Filename: "api_options.proto", 137 | }, 138 | { 139 | ExtendedType: (*descriptorpb.MessageOptions)(nil), 140 | ExtensionType: (*uint32)(nil), 141 | Field: 1036, 142 | Name: "id", 143 | Tag: "varint,1036,opt,name=id,def=0", 144 | Filename: "api_options.proto", 145 | }, 146 | { 147 | ExtendedType: (*descriptorpb.MessageOptions)(nil), 148 | ExtensionType: (*APISourceType)(nil), 149 | Field: 1037, 150 | Name: "source", 151 | Tag: "varint,1037,opt,name=source,enum=APISourceType,def=0", 152 | Filename: "api_options.proto", 153 | }, 154 | { 155 | ExtendedType: (*descriptorpb.MessageOptions)(nil), 156 | ExtensionType: (*string)(nil), 157 | Field: 1038, 158 | Name: "ifdef", 159 | Tag: "bytes,1038,opt,name=ifdef", 160 | Filename: "api_options.proto", 161 | }, 162 | { 163 | ExtendedType: (*descriptorpb.MessageOptions)(nil), 164 | ExtensionType: (*bool)(nil), 165 | Field: 1039, 166 | Name: "log", 167 | Tag: "varint,1039,opt,name=log,def=1", 168 | Filename: "api_options.proto", 169 | }, 170 | { 171 | ExtendedType: (*descriptorpb.MessageOptions)(nil), 172 | ExtensionType: (*bool)(nil), 173 | Field: 1040, 174 | Name: "no_delay", 175 | Tag: "varint,1040,opt,name=no_delay,def=0", 176 | Filename: "api_options.proto", 177 | }, 178 | } 179 | 180 | // Extension fields to descriptorpb.MethodOptions. 181 | var ( 182 | // optional bool needs_setup_connection = 1038; 183 | E_NeedsSetupConnection = &file_api_options_proto_extTypes[0] 184 | // optional bool needs_authentication = 1039; 185 | E_NeedsAuthentication = &file_api_options_proto_extTypes[1] 186 | ) 187 | 188 | // Extension fields to descriptorpb.MessageOptions. 189 | var ( 190 | // optional uint32 id = 1036; 191 | E_Id = &file_api_options_proto_extTypes[2] 192 | // optional APISourceType source = 1037; 193 | E_Source = &file_api_options_proto_extTypes[3] 194 | // optional string ifdef = 1038; 195 | E_Ifdef = &file_api_options_proto_extTypes[4] 196 | // optional bool log = 1039; 197 | E_Log = &file_api_options_proto_extTypes[5] 198 | // optional bool no_delay = 1040; 199 | E_NoDelay = &file_api_options_proto_extTypes[6] 200 | ) 201 | 202 | var File_api_options_proto protoreflect.FileDescriptor 203 | 204 | var file_api_options_proto_rawDesc = []byte{ 205 | 0x0a, 0x11, 0x61, 0x70, 0x69, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 206 | 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 207 | 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x2e, 208 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x06, 0x0a, 0x04, 0x76, 0x6f, 0x69, 0x64, 0x2a, 0x46, 0x0a, 209 | 0x0d, 0x41, 0x50, 0x49, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0f, 210 | 0x0a, 0x0b, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x42, 0x4f, 0x54, 0x48, 0x10, 0x00, 0x12, 211 | 0x11, 0x0a, 0x0d, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x53, 0x45, 0x52, 0x56, 0x45, 0x52, 212 | 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4c, 0x49, 213 | 0x45, 0x4e, 0x54, 0x10, 0x02, 0x3a, 0x5b, 0x0a, 0x16, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x5f, 0x73, 214 | 0x65, 0x74, 0x75, 0x70, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 215 | 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 216 | 0x66, 0x2e, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 217 | 0x8e, 0x08, 0x20, 0x01, 0x28, 0x08, 0x3a, 0x04, 0x74, 0x72, 0x75, 0x65, 0x52, 0x14, 0x6e, 0x65, 218 | 0x65, 0x64, 0x73, 0x53, 0x65, 0x74, 0x75, 0x70, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 219 | 0x6f, 0x6e, 0x3a, 0x58, 0x0a, 0x14, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x5f, 0x61, 0x75, 0x74, 0x68, 220 | 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1e, 0x2e, 0x67, 0x6f, 0x6f, 221 | 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x74, 222 | 0x68, 0x6f, 0x64, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x8f, 0x08, 0x20, 0x01, 0x28, 223 | 0x08, 0x3a, 0x04, 0x74, 0x72, 0x75, 0x65, 0x52, 0x13, 0x6e, 0x65, 0x65, 0x64, 0x73, 0x41, 0x75, 224 | 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x3a, 0x33, 0x0a, 0x02, 225 | 0x69, 0x64, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 226 | 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x69, 227 | 0x6f, 0x6e, 0x73, 0x18, 0x8c, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x3a, 0x01, 0x30, 0x52, 0x02, 0x69, 228 | 0x64, 0x3a, 0x55, 0x0a, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 229 | 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 230 | 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x8d, 0x08, 0x20, 231 | 0x01, 0x28, 0x0e, 0x32, 0x0e, 0x2e, 0x41, 0x50, 0x49, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 232 | 0x79, 0x70, 0x65, 0x3a, 0x0b, 0x53, 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x42, 0x4f, 0x54, 0x48, 233 | 0x52, 0x06, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x3a, 0x36, 0x0a, 0x05, 0x69, 0x66, 0x64, 0x65, 234 | 0x66, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 235 | 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 236 | 0x6e, 0x73, 0x18, 0x8e, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x66, 0x64, 0x65, 0x66, 237 | 0x3a, 0x38, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 238 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 239 | 0x65, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x8f, 0x08, 0x20, 0x01, 0x28, 0x08, 0x3a, 240 | 0x04, 0x74, 0x72, 0x75, 0x65, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x3a, 0x42, 0x0a, 0x08, 0x6e, 0x6f, 241 | 0x5f, 0x64, 0x65, 0x6c, 0x61, 0x79, 0x12, 0x1f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 242 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 243 | 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x90, 0x08, 0x20, 0x01, 0x28, 0x08, 0x3a, 0x05, 244 | 0x66, 0x61, 0x6c, 0x73, 0x65, 0x52, 0x07, 0x6e, 0x6f, 0x44, 0x65, 0x6c, 0x61, 0x79, 245 | } 246 | 247 | var ( 248 | file_api_options_proto_rawDescOnce sync.Once 249 | file_api_options_proto_rawDescData = file_api_options_proto_rawDesc 250 | ) 251 | 252 | func file_api_options_proto_rawDescGZIP() []byte { 253 | file_api_options_proto_rawDescOnce.Do(func() { 254 | file_api_options_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_options_proto_rawDescData) 255 | }) 256 | return file_api_options_proto_rawDescData 257 | } 258 | 259 | var file_api_options_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 260 | var file_api_options_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 261 | var file_api_options_proto_goTypes = []interface{}{ 262 | (APISourceType)(0), // 0: APISourceType 263 | (*Void)(nil), // 1: void 264 | (*descriptorpb.MethodOptions)(nil), // 2: google.protobuf.MethodOptions 265 | (*descriptorpb.MessageOptions)(nil), // 3: google.protobuf.MessageOptions 266 | } 267 | var file_api_options_proto_depIdxs = []int32{ 268 | 2, // 0: needs_setup_connection:extendee -> google.protobuf.MethodOptions 269 | 2, // 1: needs_authentication:extendee -> google.protobuf.MethodOptions 270 | 3, // 2: id:extendee -> google.protobuf.MessageOptions 271 | 3, // 3: source:extendee -> google.protobuf.MessageOptions 272 | 3, // 4: ifdef:extendee -> google.protobuf.MessageOptions 273 | 3, // 5: log:extendee -> google.protobuf.MessageOptions 274 | 3, // 6: no_delay:extendee -> google.protobuf.MessageOptions 275 | 0, // 7: source:type_name -> APISourceType 276 | 8, // [8:8] is the sub-list for method output_type 277 | 8, // [8:8] is the sub-list for method input_type 278 | 7, // [7:8] is the sub-list for extension type_name 279 | 0, // [0:7] is the sub-list for extension extendee 280 | 0, // [0:0] is the sub-list for field type_name 281 | } 282 | 283 | func init() { file_api_options_proto_init() } 284 | func file_api_options_proto_init() { 285 | if File_api_options_proto != nil { 286 | return 287 | } 288 | if !protoimpl.UnsafeEnabled { 289 | file_api_options_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 290 | switch v := v.(*Void); i { 291 | case 0: 292 | return &v.state 293 | case 1: 294 | return &v.sizeCache 295 | case 2: 296 | return &v.unknownFields 297 | default: 298 | return nil 299 | } 300 | } 301 | } 302 | type x struct{} 303 | out := protoimpl.TypeBuilder{ 304 | File: protoimpl.DescBuilder{ 305 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 306 | RawDescriptor: file_api_options_proto_rawDesc, 307 | NumEnums: 1, 308 | NumMessages: 1, 309 | NumExtensions: 7, 310 | NumServices: 0, 311 | }, 312 | GoTypes: file_api_options_proto_goTypes, 313 | DependencyIndexes: file_api_options_proto_depIdxs, 314 | EnumInfos: file_api_options_proto_enumTypes, 315 | MessageInfos: file_api_options_proto_msgTypes, 316 | ExtensionInfos: file_api_options_proto_extTypes, 317 | }.Build() 318 | File_api_options_proto = out.File 319 | file_api_options_proto_rawDesc = nil 320 | file_api_options_proto_goTypes = nil 321 | file_api_options_proto_depIdxs = nil 322 | } 323 | -------------------------------------------------------------------------------- /pkg/api/helper.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "reflect" 5 | 6 | "google.golang.org/protobuf/proto" 7 | ) 8 | 9 | // Request and response types from/to esphome 10 | const ( 11 | UndefinedTypeID = iota 12 | HelloRequestTypeID 13 | HelloResponseTypeID 14 | ConnectRequestTypeID 15 | ConnectResponseTypeID 16 | DisconnectRequestTypeID 17 | DisconnectResponseTypeID 18 | PingRequestTypeID 19 | PingResponseTypeID 20 | DeviceInfoRequestTypeID 21 | DeviceInfoResponseTypeID 22 | ListEntitiesRequestTypeID 23 | ListEntitiesBinarySensorResponseTypeID 24 | ListEntitiesCoverResponseTypeID 25 | ListEntitiesFanResponseTypeID 26 | ListEntitiesLightResponseTypeID 27 | ListEntitiesSensorResponseTypeID 28 | ListEntitiesSwitchResponseTypeID 29 | ListEntitiesTextSensorResponseTypeID 30 | ListEntitiesDoneResponseTypeID 31 | SubscribeStatesRequestTypeID 32 | BinarySensorStateResponseTypeID 33 | CoverStateResponseTypeID 34 | FanStateResponseTypeID 35 | LightStateResponseTypeID 36 | SensorStateResponseTypeID 37 | SwitchStateResponseTypeID 38 | TextSensorStateResponseTypeID 39 | SubscribeLogsRequestTypeID 40 | SubscribeLogsResponseTypeID 41 | CoverCommandRequestTypeID 42 | FanCommandRequestTypeID 43 | LightCommandRequestTypeID 44 | SwitchCommandRequestTypeID 45 | SubscribeHomeAssistantServicesRequestTypeID 46 | HomeAssistantServiceResponseTypeID 47 | GetTimeRequestTypeID 48 | GetTimeResponseTypeID 49 | SubscribeHomeAssistantStatesRequestTypeID 50 | SubscribeHomeAssistantStateResponseTypeID 51 | HomeAssistantStateResponseTypeID 52 | ListEntitiesServicesResponseTypeID 53 | ExecuteServiceRequestTypeID 54 | ListEntitiesCameraResponseTypeID 55 | CameraImageResponseTypeID 56 | CameraImageRequestTypeID 57 | ListEntitiesClimateResponseTypeID 58 | ClimateStateResponseTypeID 59 | ClimateCommandRequestTypeID 60 | ListEntitiesNumberResponseTypeID 61 | NumberStateResponseTypeID 62 | NumberCommandRequestTypeID 63 | ListEntitiesSelectResponseTypeID 64 | SelectStateResponseTypeID 65 | SelectCommandRequestTypeID 66 | UnknownTypeID55 67 | UnknownTypeID56 68 | UnknownTypeID57 69 | ListEntitiesLockResponseTypeID 70 | LockStateResponseTypeID 71 | LockCommandRequestTypeID 72 | ListEntitiesButtonResponseTypeID 73 | ButtonCommandRequestTypeID 74 | ListEntitiesMediaPlayerResponseTypeID 75 | MediaPlayerStateResponseTypeID 76 | MediaPlayerCommandRequestTypeID 77 | SubscribeBluetoothLEAdvertisementsRequestID 78 | BluetoothLEAdvertisementResponseID 79 | BluetoothDeviceRequestID 80 | BluetoothDeviceConnectionResponseID 81 | BluetoothGATTGetServicesRequestID 82 | BluetoothGATTGetServicesResponseID 83 | BluetoothGATTGetServicesDoneResponseID 84 | BluetoothGATTReadRequestID 85 | BluetoothGATTReadResponseID 86 | BluetoothGATTWriteRequestID 87 | BluetoothGATTReadDescriptorRequestID 88 | BluetoothGATTWriteDescriptorRequestID 89 | BluetoothGATTNotifyRequestID 90 | BluetoothGATTNotifyDataResponseID 91 | SubscribeBluetoothConnectionsFreeRequestID 92 | BluetoothConnectionsFreeResponseID 93 | BluetoothGATTErrorResponseID 94 | BluetoothGATTWriteResponseID 95 | BluetoothGATTNotifyResponseID 96 | ) 97 | 98 | func TypeID(message interface{}) uint64 { 99 | if message == nil { 100 | return UndefinedTypeID 101 | } 102 | 103 | // convert from pointer to normal type 104 | if reflect.ValueOf(message).Kind() == reflect.Ptr { 105 | message = reflect.ValueOf(message).Elem().Interface() 106 | } 107 | switch message.(type) { 108 | 109 | case HelloRequest: 110 | return HelloRequestTypeID 111 | 112 | case HelloResponse: 113 | return HelloResponseTypeID 114 | 115 | case ConnectRequest: 116 | return ConnectRequestTypeID 117 | 118 | case ConnectResponse: 119 | return ConnectResponseTypeID 120 | 121 | case DisconnectRequest: 122 | return DisconnectRequestTypeID 123 | 124 | case DisconnectResponse: 125 | return DisconnectResponseTypeID 126 | 127 | case PingRequest: 128 | return PingRequestTypeID 129 | 130 | case PingResponse: 131 | return PingResponseTypeID 132 | 133 | case DeviceInfoRequest: 134 | return DeviceInfoRequestTypeID 135 | 136 | case DeviceInfoResponse: 137 | return DeviceInfoResponseTypeID 138 | 139 | case ListEntitiesRequest: 140 | return ListEntitiesRequestTypeID 141 | 142 | case ListEntitiesBinarySensorResponse: 143 | return ListEntitiesBinarySensorResponseTypeID 144 | 145 | case ListEntitiesCoverResponse: 146 | return ListEntitiesCoverResponseTypeID 147 | 148 | case ListEntitiesFanResponse: 149 | return ListEntitiesFanResponseTypeID 150 | 151 | case ListEntitiesLightResponse: 152 | return ListEntitiesLightResponseTypeID 153 | 154 | case ListEntitiesSensorResponse: 155 | return ListEntitiesSensorResponseTypeID 156 | 157 | case ListEntitiesSwitchResponse: 158 | return ListEntitiesSwitchResponseTypeID 159 | 160 | case ListEntitiesTextSensorResponse: 161 | return ListEntitiesTextSensorResponseTypeID 162 | 163 | case ListEntitiesDoneResponse: 164 | return ListEntitiesDoneResponseTypeID 165 | 166 | case SubscribeStatesRequest: 167 | return SubscribeStatesRequestTypeID 168 | 169 | case BinarySensorStateResponse: 170 | return BinarySensorStateResponseTypeID 171 | 172 | case CoverStateResponse: 173 | return CoverStateResponseTypeID 174 | 175 | case FanStateResponse: 176 | return FanStateResponseTypeID 177 | 178 | case LightStateResponse: 179 | return LightStateResponseTypeID 180 | 181 | case SensorStateResponse: 182 | return SensorStateResponseTypeID 183 | 184 | case SwitchStateResponse: 185 | return SwitchStateResponseTypeID 186 | 187 | case TextSensorStateResponse: 188 | return TextSensorStateResponseTypeID 189 | 190 | case SubscribeLogsRequest: 191 | return SubscribeLogsRequestTypeID 192 | 193 | case SubscribeLogsResponse: 194 | return SubscribeLogsResponseTypeID 195 | 196 | case CoverCommandRequest: 197 | return CoverCommandRequestTypeID 198 | 199 | case FanCommandRequest: 200 | return FanCommandRequestTypeID 201 | 202 | case LightCommandRequest: 203 | return LightCommandRequestTypeID 204 | 205 | case SwitchCommandRequest: 206 | return SwitchCommandRequestTypeID 207 | 208 | case SubscribeHomeassistantServicesRequest: 209 | return SubscribeHomeAssistantServicesRequestTypeID 210 | 211 | case HomeassistantServiceResponse: 212 | return HomeAssistantServiceResponseTypeID 213 | 214 | case GetTimeRequest: 215 | return GetTimeRequestTypeID 216 | 217 | case GetTimeResponse: 218 | return GetTimeResponseTypeID 219 | 220 | case SubscribeHomeAssistantStatesRequest: 221 | return SubscribeHomeAssistantStatesRequestTypeID 222 | 223 | case SubscribeHomeAssistantStateResponse: 224 | return SubscribeHomeAssistantStateResponseTypeID 225 | 226 | case HomeAssistantStateResponse: 227 | return HomeAssistantStateResponseTypeID 228 | 229 | case ListEntitiesServicesResponse: 230 | return ListEntitiesServicesResponseTypeID 231 | 232 | case ExecuteServiceRequest: 233 | return ExecuteServiceRequestTypeID 234 | 235 | case ListEntitiesCameraResponse: 236 | return ListEntitiesCameraResponseTypeID 237 | 238 | case CameraImageResponse: 239 | return CameraImageResponseTypeID 240 | 241 | case CameraImageRequest: 242 | return CameraImageRequestTypeID 243 | 244 | case ListEntitiesClimateResponse: 245 | return ListEntitiesClimateResponseTypeID 246 | 247 | case ClimateStateResponse: 248 | return ClimateStateResponseTypeID 249 | 250 | case ClimateCommandRequest: 251 | return ClimateCommandRequestTypeID 252 | 253 | case NumberCommandRequest: 254 | return ClimateCommandRequestTypeID 255 | 256 | case SelectCommandRequest: 257 | return SelectCommandRequestTypeID 258 | 259 | case ButtonCommandRequest: 260 | return ButtonCommandRequestTypeID 261 | 262 | case LockCommandRequest: 263 | return LockCommandRequestTypeID 264 | 265 | case MediaPlayerCommandRequest: 266 | return MediaPlayerCommandRequestTypeID 267 | 268 | case ListEntitiesNumberResponse: 269 | return ListEntitiesNumberResponseTypeID 270 | 271 | case NumberStateResponse: 272 | return NumberStateResponseTypeID 273 | 274 | case ListEntitiesSelectResponse: 275 | return ListEntitiesSelectResponseTypeID 276 | 277 | case SelectStateResponse: 278 | return SelectStateResponseTypeID 279 | 280 | case ListEntitiesLockResponse: 281 | return ListEntitiesLockResponseTypeID 282 | 283 | case LockStateResponse: 284 | return LockStateResponseTypeID 285 | 286 | case ListEntitiesButtonResponse: 287 | return ListEntitiesButtonResponseTypeID 288 | 289 | case ListEntitiesMediaPlayerResponse: 290 | return ListEntitiesMediaPlayerResponseTypeID 291 | 292 | case MediaPlayerStateResponse: 293 | return MediaPlayerStateResponseTypeID 294 | 295 | case SubscribeBluetoothLEAdvertisementsRequest: 296 | return SubscribeBluetoothLEAdvertisementsRequestID 297 | 298 | case BluetoothLEAdvertisementResponse: 299 | return BluetoothLEAdvertisementResponseID 300 | 301 | case BluetoothDeviceRequest: 302 | return BluetoothDeviceRequestID 303 | 304 | case BluetoothDeviceConnectionResponse: 305 | return BluetoothDeviceConnectionResponseID 306 | 307 | case BluetoothGATTGetServicesRequest: 308 | return BluetoothGATTGetServicesRequestID 309 | 310 | case BluetoothGATTGetServicesResponse: 311 | return BluetoothGATTGetServicesResponseID 312 | 313 | case BluetoothGATTGetServicesDoneResponse: 314 | return BluetoothGATTGetServicesDoneResponseID 315 | 316 | case BluetoothGATTReadRequest: 317 | return BluetoothGATTReadRequestID 318 | 319 | case BluetoothGATTReadResponse: 320 | return BluetoothGATTReadResponseID 321 | 322 | case BluetoothGATTWriteRequest: 323 | return BluetoothGATTWriteRequestID 324 | 325 | case BluetoothGATTReadDescriptorRequest: 326 | return BluetoothGATTReadDescriptorRequestID 327 | 328 | case BluetoothGATTWriteDescriptorRequest: 329 | return BluetoothGATTWriteDescriptorRequestID 330 | 331 | case BluetoothGATTNotifyRequest: 332 | return BluetoothGATTNotifyRequestID 333 | 334 | case BluetoothGATTNotifyDataResponse: 335 | return BluetoothGATTNotifyDataResponseID 336 | 337 | case SubscribeBluetoothConnectionsFreeRequest: 338 | return SubscribeBluetoothConnectionsFreeRequestID 339 | 340 | case BluetoothConnectionsFreeResponse: 341 | return BluetoothConnectionsFreeResponseID 342 | 343 | case BluetoothGATTErrorResponse: 344 | return BluetoothGATTErrorResponseID 345 | 346 | case BluetoothGATTWriteResponse: 347 | return BluetoothGATTWriteResponseID 348 | 349 | case BluetoothGATTNotifyResponse: 350 | return BluetoothGATTNotifyResponseID 351 | 352 | default: 353 | return UndefinedTypeID 354 | } 355 | } 356 | 357 | func NewMessageByTypeID(typeID uint64) proto.Message { 358 | switch typeID { 359 | 360 | case 1: 361 | return new(HelloRequest) 362 | 363 | case 2: 364 | return new(HelloResponse) 365 | 366 | case 3: 367 | return new(ConnectRequest) 368 | 369 | case 4: 370 | return new(ConnectResponse) 371 | 372 | case 5: 373 | return new(DisconnectRequest) 374 | 375 | case 6: 376 | return new(DisconnectResponse) 377 | 378 | case 7: 379 | return new(PingRequest) 380 | 381 | case 8: 382 | return new(PingResponse) 383 | 384 | case 9: 385 | return new(DeviceInfoRequest) 386 | 387 | case 10: 388 | return new(DeviceInfoResponse) 389 | 390 | case 11: 391 | return new(ListEntitiesRequest) 392 | 393 | case 12: 394 | return new(ListEntitiesBinarySensorResponse) 395 | 396 | case 13: 397 | return new(ListEntitiesCoverResponse) 398 | 399 | case 14: 400 | return new(ListEntitiesFanResponse) 401 | 402 | case 15: 403 | return new(ListEntitiesLightResponse) 404 | 405 | case 16: 406 | return new(ListEntitiesSensorResponse) 407 | 408 | case 17: 409 | return new(ListEntitiesSwitchResponse) 410 | 411 | case 18: 412 | return new(ListEntitiesTextSensorResponse) 413 | 414 | case 19: 415 | return new(ListEntitiesDoneResponse) 416 | 417 | case 20: 418 | return new(SubscribeStatesRequest) 419 | 420 | case 21: 421 | return new(BinarySensorStateResponse) 422 | 423 | case 22: 424 | return new(CoverStateResponse) 425 | 426 | case 23: 427 | return new(FanStateResponse) 428 | 429 | case 24: 430 | return new(LightStateResponse) 431 | 432 | case 25: 433 | return new(SensorStateResponse) 434 | 435 | case 26: 436 | return new(SwitchStateResponse) 437 | 438 | case 27: 439 | return new(TextSensorStateResponse) 440 | 441 | case 28: 442 | return new(SubscribeLogsRequest) 443 | 444 | case 29: 445 | return new(SubscribeLogsResponse) 446 | 447 | case 30: 448 | return new(CoverCommandRequest) 449 | 450 | case 31: 451 | return new(FanCommandRequest) 452 | 453 | case 32: 454 | return new(LightCommandRequest) 455 | 456 | case 33: 457 | return new(SwitchCommandRequest) 458 | 459 | case 34: 460 | return new(SubscribeHomeassistantServicesRequest) 461 | 462 | case 35: 463 | return new(HomeassistantServiceResponse) 464 | 465 | case 36: 466 | return new(GetTimeRequest) 467 | 468 | case 37: 469 | return new(GetTimeResponse) 470 | 471 | case 38: 472 | return new(SubscribeHomeAssistantStatesRequest) 473 | 474 | case 39: 475 | return new(SubscribeHomeAssistantStateResponse) 476 | 477 | case 40: 478 | return new(HomeAssistantStateResponse) 479 | 480 | case 41: 481 | return new(ListEntitiesServicesResponse) 482 | 483 | case 42: 484 | return new(ExecuteServiceRequest) 485 | 486 | case 43: 487 | return new(ListEntitiesCameraResponse) 488 | 489 | case 44: 490 | return new(CameraImageResponse) 491 | 492 | case 45: 493 | return new(CameraImageRequest) 494 | 495 | case 46: 496 | return new(ListEntitiesClimateResponse) 497 | 498 | case 47: 499 | return new(ClimateStateResponse) 500 | 501 | case 48: 502 | return new(ClimateCommandRequest) 503 | 504 | case 49: 505 | return new(ListEntitiesNumberResponse) 506 | 507 | case 50: 508 | return new(NumberStateResponse) 509 | 510 | case 51: 511 | return new(NumberCommandRequest) 512 | 513 | case 52: 514 | return new(ListEntitiesSelectResponse) 515 | 516 | case 53: 517 | return new(SelectStateResponse) 518 | 519 | case 54: 520 | return new(SelectCommandRequest) 521 | 522 | // case 55: 523 | // return new(UnknownTypeID55) 524 | // 525 | // case 56: 526 | // return new(UnknownTypeID56) 527 | // 528 | // case 57: 529 | // return new(UnknownTypeID57) 530 | 531 | case 58: 532 | return new(ListEntitiesLockResponse) 533 | 534 | case 59: 535 | return new(LockStateResponse) 536 | 537 | case 60: 538 | return new(LockCommandRequest) 539 | 540 | case 61: 541 | return new(ListEntitiesButtonResponse) 542 | 543 | case 62: 544 | return new(ButtonCommandRequest) 545 | 546 | case 63: 547 | return new(ListEntitiesMediaPlayerResponse) 548 | 549 | case 64: 550 | return new(MediaPlayerStateResponse) 551 | 552 | case 65: 553 | return new(MediaPlayerCommandRequest) 554 | 555 | case 66: 556 | return new(SubscribeBluetoothLEAdvertisementsRequest) 557 | 558 | case 67: 559 | return new(BluetoothLEAdvertisementResponse) 560 | 561 | case 68: 562 | return new(BluetoothDeviceRequest) 563 | 564 | case 69: 565 | return new(BluetoothDeviceConnectionResponse) 566 | 567 | case 70: 568 | return new(BluetoothGATTGetServicesRequest) 569 | 570 | case 71: 571 | return new(BluetoothGATTGetServicesResponse) 572 | 573 | case 72: 574 | return new(BluetoothGATTGetServicesDoneResponse) 575 | 576 | case 73: 577 | return new(BluetoothGATTReadRequest) 578 | 579 | case 74: 580 | return new(BluetoothGATTReadResponse) 581 | 582 | case 75: 583 | return new(BluetoothGATTWriteRequest) 584 | 585 | case 76: 586 | return new(BluetoothGATTReadDescriptorRequest) 587 | 588 | case 77: 589 | return new(BluetoothGATTWriteDescriptorRequest) 590 | 591 | case 78: 592 | return new(BluetoothGATTNotifyRequest) 593 | 594 | case 79: 595 | return new(BluetoothGATTNotifyDataResponse) 596 | 597 | case 80: 598 | return new(SubscribeBluetoothConnectionsFreeRequest) 599 | 600 | case 81: 601 | return new(BluetoothConnectionsFreeResponse) 602 | 603 | case 82: 604 | return new(BluetoothGATTErrorResponse) 605 | 606 | case 83: 607 | return new(BluetoothGATTWriteResponse) 608 | 609 | case 84: 610 | return new(BluetoothGATTNotifyResponse) 611 | 612 | default: 613 | return nil 614 | } 615 | } 616 | -------------------------------------------------------------------------------- /proto/api.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "api_options.proto"; 4 | 5 | service APIConnection { 6 | rpc hello (HelloRequest) returns (HelloResponse) { 7 | option (needs_setup_connection) = false; 8 | option (needs_authentication) = false; 9 | } 10 | rpc connect (ConnectRequest) returns (ConnectResponse) { 11 | option (needs_setup_connection) = false; 12 | option (needs_authentication) = false; 13 | } 14 | rpc disconnect (DisconnectRequest) returns (DisconnectResponse) { 15 | option (needs_setup_connection) = false; 16 | option (needs_authentication) = false; 17 | } 18 | rpc ping (PingRequest) returns (PingResponse) { 19 | option (needs_setup_connection) = false; 20 | option (needs_authentication) = false; 21 | } 22 | rpc device_info (DeviceInfoRequest) returns (DeviceInfoResponse) { 23 | option (needs_authentication) = false; 24 | } 25 | rpc list_entities (ListEntitiesRequest) returns (void) {} 26 | rpc subscribe_states (SubscribeStatesRequest) returns (void) {} 27 | rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} 28 | rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} 29 | rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} 30 | rpc get_time (GetTimeRequest) returns (GetTimeResponse) { 31 | option (needs_authentication) = false; 32 | } 33 | rpc execute_service (ExecuteServiceRequest) returns (void) {} 34 | 35 | rpc cover_command (CoverCommandRequest) returns (void) {} 36 | rpc fan_command (FanCommandRequest) returns (void) {} 37 | rpc light_command (LightCommandRequest) returns (void) {} 38 | rpc switch_command (SwitchCommandRequest) returns (void) {} 39 | rpc camera_image (CameraImageRequest) returns (void) {} 40 | rpc climate_command (ClimateCommandRequest) returns (void) {} 41 | rpc number_command (NumberCommandRequest) returns (void) {} 42 | rpc text_command (TextCommandRequest) returns (void) {} 43 | rpc select_command (SelectCommandRequest) returns (void) {} 44 | rpc button_command (ButtonCommandRequest) returns (void) {} 45 | rpc lock_command (LockCommandRequest) returns (void) {} 46 | rpc valve_command (ValveCommandRequest) returns (void) {} 47 | rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} 48 | rpc date_command (DateCommandRequest) returns (void) {} 49 | rpc time_command (TimeCommandRequest) returns (void) {} 50 | rpc datetime_command (DateTimeCommandRequest) returns (void) {} 51 | 52 | rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} 53 | rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} 54 | rpc bluetooth_gatt_get_services(BluetoothGATTGetServicesRequest) returns (void) {} 55 | rpc bluetooth_gatt_read(BluetoothGATTReadRequest) returns (void) {} 56 | rpc bluetooth_gatt_write(BluetoothGATTWriteRequest) returns (void) {} 57 | rpc bluetooth_gatt_read_descriptor(BluetoothGATTReadDescriptorRequest) returns (void) {} 58 | rpc bluetooth_gatt_write_descriptor(BluetoothGATTWriteDescriptorRequest) returns (void) {} 59 | rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {} 60 | rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} 61 | rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} 62 | 63 | rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} 64 | 65 | rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {} 66 | } 67 | 68 | 69 | // ==================== BASE PACKETS ==================== 70 | 71 | // The Home Assistant protocol is structured as a simple 72 | // TCP socket with short binary messages encoded in the protocol buffers format 73 | // First, a message in this protocol has a specific format: 74 | // * A zero byte. 75 | // * VarInt denoting the size of the message object. (type is not part of this) 76 | // * VarInt denoting the type of message. 77 | // * The message object encoded as a ProtoBuf message 78 | 79 | // The connection is established in 4 steps: 80 | // * First, the client connects to the server and sends a "Hello Request" identifying itself 81 | // * The server responds with a "Hello Response" and selects the protocol version 82 | // * After receiving this message, the client attempts to authenticate itself using 83 | // the password and a "Connect Request" 84 | // * The server responds with a "Connect Response" and notifies of invalid password. 85 | // If anything in this initial process fails, the connection must immediately closed 86 | // by both sides and _no_ disconnection message is to be sent. 87 | 88 | // Message sent at the beginning of each connection 89 | // Can only be sent by the client and only at the beginning of the connection 90 | message HelloRequest { 91 | option (id) = 1; 92 | option (source) = SOURCE_CLIENT; 93 | option (no_delay) = true; 94 | 95 | // Description of client (like User Agent) 96 | // For example "Home Assistant" 97 | // Not strictly necessary to send but nice for debugging 98 | // purposes. 99 | string client_info = 1; 100 | uint32 api_version_major = 2; 101 | uint32 api_version_minor = 3; 102 | } 103 | 104 | // Confirmation of successful connection request. 105 | // Can only be sent by the server and only at the beginning of the connection 106 | message HelloResponse { 107 | option (id) = 2; 108 | option (source) = SOURCE_SERVER; 109 | option (no_delay) = true; 110 | 111 | // The version of the API to use. The _client_ (for example Home Assistant) needs to check 112 | // for compatibility and if necessary adopt to an older API. 113 | // Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_ 114 | // Minor is for breaking changes in individual messages - a mismatch will lead to a warning message 115 | uint32 api_version_major = 1; 116 | uint32 api_version_minor = 2; 117 | 118 | // A string identifying the server (ESP); like client info this may be empty 119 | // and only exists for debugging/logging purposes. 120 | // For example "ESPHome v1.10.0 on ESP8266" 121 | string server_info = 3; 122 | 123 | // The name of the server (App.get_name()) 124 | string name = 4; 125 | } 126 | 127 | // Message sent at the beginning of each connection to authenticate the client 128 | // Can only be sent by the client and only at the beginning of the connection 129 | message ConnectRequest { 130 | option (id) = 3; 131 | option (source) = SOURCE_CLIENT; 132 | option (no_delay) = true; 133 | 134 | // The password to log in with 135 | string password = 1; 136 | } 137 | 138 | // Confirmation of successful connection. After this the connection is available for all traffic. 139 | // Can only be sent by the server and only at the beginning of the connection 140 | message ConnectResponse { 141 | option (id) = 4; 142 | option (source) = SOURCE_SERVER; 143 | option (no_delay) = true; 144 | 145 | bool invalid_password = 1; 146 | } 147 | 148 | // Request to close the connection. 149 | // Can be sent by both the client and server 150 | message DisconnectRequest { 151 | option (id) = 5; 152 | option (source) = SOURCE_BOTH; 153 | option (no_delay) = true; 154 | 155 | // Do not close the connection before the acknowledgement arrives 156 | } 157 | 158 | message DisconnectResponse { 159 | option (id) = 6; 160 | option (source) = SOURCE_BOTH; 161 | option (no_delay) = true; 162 | 163 | // Empty - Both parties are required to close the connection after this 164 | // message has been received. 165 | } 166 | 167 | message PingRequest { 168 | option (id) = 7; 169 | option (source) = SOURCE_BOTH; 170 | // Empty 171 | } 172 | 173 | message PingResponse { 174 | option (id) = 8; 175 | option (source) = SOURCE_BOTH; 176 | // Empty 177 | } 178 | 179 | message DeviceInfoRequest { 180 | option (id) = 9; 181 | option (source) = SOURCE_CLIENT; 182 | // Empty 183 | } 184 | 185 | message DeviceInfoResponse { 186 | option (id) = 10; 187 | option (source) = SOURCE_SERVER; 188 | 189 | bool uses_password = 1; 190 | 191 | // The name of the node, given by "App.set_name()" 192 | string name = 2; 193 | 194 | // The mac address of the device. For example "AC:BC:32:89:0E:A9" 195 | string mac_address = 3; 196 | 197 | // A string describing the ESPHome version. For example "1.10.0" 198 | string esphome_version = 4; 199 | 200 | // A string describing the date of compilation, this is generated by the compiler 201 | // and therefore may not be in the same format all the time. 202 | // If the user isn't using ESPHome, this will also not be set. 203 | string compilation_time = 5; 204 | 205 | // The model of the board. For example NodeMCU 206 | string model = 6; 207 | 208 | bool has_deep_sleep = 7; 209 | 210 | // The esphome project details if set 211 | string project_name = 8; 212 | string project_version = 9; 213 | 214 | uint32 webserver_port = 10; 215 | 216 | uint32 legacy_bluetooth_proxy_version = 11; 217 | uint32 bluetooth_proxy_feature_flags = 15; 218 | 219 | string manufacturer = 12; 220 | 221 | string friendly_name = 13; 222 | 223 | uint32 legacy_voice_assistant_version = 14; 224 | uint32 voice_assistant_feature_flags = 17; 225 | 226 | string suggested_area = 16; 227 | } 228 | 229 | message ListEntitiesRequest { 230 | option (id) = 11; 231 | option (source) = SOURCE_CLIENT; 232 | // Empty 233 | } 234 | message ListEntitiesDoneResponse { 235 | option (id) = 19; 236 | option (source) = SOURCE_SERVER; 237 | option (no_delay) = true; 238 | // Empty 239 | } 240 | message SubscribeStatesRequest { 241 | option (id) = 20; 242 | option (source) = SOURCE_CLIENT; 243 | // Empty 244 | } 245 | 246 | // ==================== COMMON ===================== 247 | 248 | enum EntityCategory { 249 | ENTITY_CATEGORY_NONE = 0; 250 | ENTITY_CATEGORY_CONFIG = 1; 251 | ENTITY_CATEGORY_DIAGNOSTIC = 2; 252 | } 253 | 254 | // ==================== BINARY SENSOR ==================== 255 | message ListEntitiesBinarySensorResponse { 256 | option (id) = 12; 257 | option (source) = SOURCE_SERVER; 258 | option (ifdef) = "USE_BINARY_SENSOR"; 259 | 260 | string object_id = 1; 261 | fixed32 key = 2; 262 | string name = 3; 263 | string unique_id = 4; 264 | 265 | string device_class = 5; 266 | bool is_status_binary_sensor = 6; 267 | bool disabled_by_default = 7; 268 | string icon = 8; 269 | EntityCategory entity_category = 9; 270 | } 271 | message BinarySensorStateResponse { 272 | option (id) = 21; 273 | option (source) = SOURCE_SERVER; 274 | option (ifdef) = "USE_BINARY_SENSOR"; 275 | option (no_delay) = true; 276 | 277 | fixed32 key = 1; 278 | bool state = 2; 279 | // If the binary sensor does not have a valid state yet. 280 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 281 | bool missing_state = 3; 282 | } 283 | 284 | // ==================== COVER ==================== 285 | message ListEntitiesCoverResponse { 286 | option (id) = 13; 287 | option (source) = SOURCE_SERVER; 288 | option (ifdef) = "USE_COVER"; 289 | 290 | string object_id = 1; 291 | fixed32 key = 2; 292 | string name = 3; 293 | string unique_id = 4; 294 | 295 | bool assumed_state = 5; 296 | bool supports_position = 6; 297 | bool supports_tilt = 7; 298 | string device_class = 8; 299 | bool disabled_by_default = 9; 300 | string icon = 10; 301 | EntityCategory entity_category = 11; 302 | bool supports_stop = 12; 303 | } 304 | 305 | enum LegacyCoverState { 306 | LEGACY_COVER_STATE_OPEN = 0; 307 | LEGACY_COVER_STATE_CLOSED = 1; 308 | } 309 | enum CoverOperation { 310 | COVER_OPERATION_IDLE = 0; 311 | COVER_OPERATION_IS_OPENING = 1; 312 | COVER_OPERATION_IS_CLOSING = 2; 313 | } 314 | message CoverStateResponse { 315 | option (id) = 22; 316 | option (source) = SOURCE_SERVER; 317 | option (ifdef) = "USE_COVER"; 318 | option (no_delay) = true; 319 | 320 | fixed32 key = 1; 321 | // legacy: state has been removed in 1.13 322 | // clients/servers must still send/accept it until the next protocol change 323 | LegacyCoverState legacy_state = 2; 324 | 325 | float position = 3; 326 | float tilt = 4; 327 | CoverOperation current_operation = 5; 328 | } 329 | 330 | enum LegacyCoverCommand { 331 | LEGACY_COVER_COMMAND_OPEN = 0; 332 | LEGACY_COVER_COMMAND_CLOSE = 1; 333 | LEGACY_COVER_COMMAND_STOP = 2; 334 | } 335 | message CoverCommandRequest { 336 | option (id) = 30; 337 | option (source) = SOURCE_CLIENT; 338 | option (ifdef) = "USE_COVER"; 339 | option (no_delay) = true; 340 | 341 | fixed32 key = 1; 342 | 343 | // legacy: command has been removed in 1.13 344 | // clients/servers must still send/accept it until the next protocol change 345 | bool has_legacy_command = 2; 346 | LegacyCoverCommand legacy_command = 3; 347 | 348 | bool has_position = 4; 349 | float position = 5; 350 | bool has_tilt = 6; 351 | float tilt = 7; 352 | bool stop = 8; 353 | } 354 | 355 | // ==================== FAN ==================== 356 | message ListEntitiesFanResponse { 357 | option (id) = 14; 358 | option (source) = SOURCE_SERVER; 359 | option (ifdef) = "USE_FAN"; 360 | 361 | string object_id = 1; 362 | fixed32 key = 2; 363 | string name = 3; 364 | string unique_id = 4; 365 | 366 | bool supports_oscillation = 5; 367 | bool supports_speed = 6; 368 | bool supports_direction = 7; 369 | int32 supported_speed_count = 8; 370 | bool disabled_by_default = 9; 371 | string icon = 10; 372 | EntityCategory entity_category = 11; 373 | repeated string supported_preset_modes = 12; 374 | } 375 | enum FanSpeed { 376 | FAN_SPEED_LOW = 0; 377 | FAN_SPEED_MEDIUM = 1; 378 | FAN_SPEED_HIGH = 2; 379 | } 380 | enum FanDirection { 381 | FAN_DIRECTION_FORWARD = 0; 382 | FAN_DIRECTION_REVERSE = 1; 383 | } 384 | message FanStateResponse { 385 | option (id) = 23; 386 | option (source) = SOURCE_SERVER; 387 | option (ifdef) = "USE_FAN"; 388 | option (no_delay) = true; 389 | 390 | fixed32 key = 1; 391 | bool state = 2; 392 | bool oscillating = 3; 393 | FanSpeed speed = 4 [deprecated = true]; 394 | FanDirection direction = 5; 395 | int32 speed_level = 6; 396 | string preset_mode = 7; 397 | } 398 | message FanCommandRequest { 399 | option (id) = 31; 400 | option (source) = SOURCE_CLIENT; 401 | option (ifdef) = "USE_FAN"; 402 | option (no_delay) = true; 403 | 404 | fixed32 key = 1; 405 | bool has_state = 2; 406 | bool state = 3; 407 | bool has_speed = 4 [deprecated = true]; 408 | FanSpeed speed = 5 [deprecated = true]; 409 | bool has_oscillating = 6; 410 | bool oscillating = 7; 411 | bool has_direction = 8; 412 | FanDirection direction = 9; 413 | bool has_speed_level = 10; 414 | int32 speed_level = 11; 415 | bool has_preset_mode = 12; 416 | string preset_mode = 13; 417 | } 418 | 419 | // ==================== LIGHT ==================== 420 | enum ColorMode { 421 | COLOR_MODE_UNKNOWN = 0; 422 | COLOR_MODE_ON_OFF = 1; 423 | COLOR_MODE_BRIGHTNESS = 2; 424 | COLOR_MODE_WHITE = 7; 425 | COLOR_MODE_COLOR_TEMPERATURE = 11; 426 | COLOR_MODE_COLD_WARM_WHITE = 19; 427 | COLOR_MODE_RGB = 35; 428 | COLOR_MODE_RGB_WHITE = 39; 429 | COLOR_MODE_RGB_COLOR_TEMPERATURE = 47; 430 | COLOR_MODE_RGB_COLD_WARM_WHITE = 51; 431 | } 432 | message ListEntitiesLightResponse { 433 | option (id) = 15; 434 | option (source) = SOURCE_SERVER; 435 | option (ifdef) = "USE_LIGHT"; 436 | 437 | string object_id = 1; 438 | fixed32 key = 2; 439 | string name = 3; 440 | string unique_id = 4; 441 | 442 | repeated ColorMode supported_color_modes = 12; 443 | // next four supports_* are for legacy clients, newer clients should use color modes 444 | bool legacy_supports_brightness = 5 [deprecated=true]; 445 | bool legacy_supports_rgb = 6 [deprecated=true]; 446 | bool legacy_supports_white_value = 7 [deprecated=true]; 447 | bool legacy_supports_color_temperature = 8 [deprecated=true]; 448 | float min_mireds = 9; 449 | float max_mireds = 10; 450 | repeated string effects = 11; 451 | bool disabled_by_default = 13; 452 | string icon = 14; 453 | EntityCategory entity_category = 15; 454 | } 455 | message LightStateResponse { 456 | option (id) = 24; 457 | option (source) = SOURCE_SERVER; 458 | option (ifdef) = "USE_LIGHT"; 459 | option (no_delay) = true; 460 | 461 | fixed32 key = 1; 462 | bool state = 2; 463 | float brightness = 3; 464 | ColorMode color_mode = 11; 465 | float color_brightness = 10; 466 | float red = 4; 467 | float green = 5; 468 | float blue = 6; 469 | float white = 7; 470 | float color_temperature = 8; 471 | float cold_white = 12; 472 | float warm_white = 13; 473 | string effect = 9; 474 | } 475 | message LightCommandRequest { 476 | option (id) = 32; 477 | option (source) = SOURCE_CLIENT; 478 | option (ifdef) = "USE_LIGHT"; 479 | option (no_delay) = true; 480 | 481 | fixed32 key = 1; 482 | bool has_state = 2; 483 | bool state = 3; 484 | bool has_brightness = 4; 485 | float brightness = 5; 486 | bool has_color_mode = 22; 487 | ColorMode color_mode = 23; 488 | bool has_color_brightness = 20; 489 | float color_brightness = 21; 490 | bool has_rgb = 6; 491 | float red = 7; 492 | float green = 8; 493 | float blue = 9; 494 | bool has_white = 10; 495 | float white = 11; 496 | bool has_color_temperature = 12; 497 | float color_temperature = 13; 498 | bool has_cold_white = 24; 499 | float cold_white = 25; 500 | bool has_warm_white = 26; 501 | float warm_white = 27; 502 | bool has_transition_length = 14; 503 | uint32 transition_length = 15; 504 | bool has_flash_length = 16; 505 | uint32 flash_length = 17; 506 | bool has_effect = 18; 507 | string effect = 19; 508 | } 509 | 510 | // ==================== SENSOR ==================== 511 | enum SensorStateClass { 512 | STATE_CLASS_NONE = 0; 513 | STATE_CLASS_MEASUREMENT = 1; 514 | STATE_CLASS_TOTAL_INCREASING = 2; 515 | STATE_CLASS_TOTAL = 3; 516 | } 517 | 518 | enum SensorLastResetType { 519 | LAST_RESET_NONE = 0; 520 | LAST_RESET_NEVER = 1; 521 | LAST_RESET_AUTO = 2; 522 | } 523 | 524 | message ListEntitiesSensorResponse { 525 | option (id) = 16; 526 | option (source) = SOURCE_SERVER; 527 | option (ifdef) = "USE_SENSOR"; 528 | 529 | string object_id = 1; 530 | fixed32 key = 2; 531 | string name = 3; 532 | string unique_id = 4; 533 | 534 | string icon = 5; 535 | string unit_of_measurement = 6; 536 | int32 accuracy_decimals = 7; 537 | bool force_update = 8; 538 | string device_class = 9; 539 | SensorStateClass state_class = 10; 540 | // Last reset type removed in 2021.9.0 541 | SensorLastResetType legacy_last_reset_type = 11; 542 | bool disabled_by_default = 12; 543 | EntityCategory entity_category = 13; 544 | } 545 | message SensorStateResponse { 546 | option (id) = 25; 547 | option (source) = SOURCE_SERVER; 548 | option (ifdef) = "USE_SENSOR"; 549 | option (no_delay) = true; 550 | 551 | fixed32 key = 1; 552 | float state = 2; 553 | // If the sensor does not have a valid state yet. 554 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 555 | bool missing_state = 3; 556 | } 557 | 558 | // ==================== SWITCH ==================== 559 | message ListEntitiesSwitchResponse { 560 | option (id) = 17; 561 | option (source) = SOURCE_SERVER; 562 | option (ifdef) = "USE_SWITCH"; 563 | 564 | string object_id = 1; 565 | fixed32 key = 2; 566 | string name = 3; 567 | string unique_id = 4; 568 | 569 | string icon = 5; 570 | bool assumed_state = 6; 571 | bool disabled_by_default = 7; 572 | EntityCategory entity_category = 8; 573 | string device_class = 9; 574 | } 575 | message SwitchStateResponse { 576 | option (id) = 26; 577 | option (source) = SOURCE_SERVER; 578 | option (ifdef) = "USE_SWITCH"; 579 | option (no_delay) = true; 580 | 581 | fixed32 key = 1; 582 | bool state = 2; 583 | } 584 | message SwitchCommandRequest { 585 | option (id) = 33; 586 | option (source) = SOURCE_CLIENT; 587 | option (ifdef) = "USE_SWITCH"; 588 | option (no_delay) = true; 589 | 590 | fixed32 key = 1; 591 | bool state = 2; 592 | } 593 | 594 | // ==================== TEXT SENSOR ==================== 595 | message ListEntitiesTextSensorResponse { 596 | option (id) = 18; 597 | option (source) = SOURCE_SERVER; 598 | option (ifdef) = "USE_TEXT_SENSOR"; 599 | 600 | string object_id = 1; 601 | fixed32 key = 2; 602 | string name = 3; 603 | string unique_id = 4; 604 | 605 | string icon = 5; 606 | bool disabled_by_default = 6; 607 | EntityCategory entity_category = 7; 608 | string device_class = 8; 609 | } 610 | message TextSensorStateResponse { 611 | option (id) = 27; 612 | option (source) = SOURCE_SERVER; 613 | option (ifdef) = "USE_TEXT_SENSOR"; 614 | option (no_delay) = true; 615 | 616 | fixed32 key = 1; 617 | string state = 2; 618 | // If the text sensor does not have a valid state yet. 619 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 620 | bool missing_state = 3; 621 | } 622 | 623 | // ==================== SUBSCRIBE LOGS ==================== 624 | enum LogLevel { 625 | LOG_LEVEL_NONE = 0; 626 | LOG_LEVEL_ERROR = 1; 627 | LOG_LEVEL_WARN = 2; 628 | LOG_LEVEL_INFO = 3; 629 | LOG_LEVEL_CONFIG = 4; 630 | LOG_LEVEL_DEBUG = 5; 631 | LOG_LEVEL_VERBOSE = 6; 632 | LOG_LEVEL_VERY_VERBOSE = 7; 633 | } 634 | message SubscribeLogsRequest { 635 | option (id) = 28; 636 | option (source) = SOURCE_CLIENT; 637 | LogLevel level = 1; 638 | bool dump_config = 2; 639 | } 640 | message SubscribeLogsResponse { 641 | option (id) = 29; 642 | option (source) = SOURCE_SERVER; 643 | option (log) = false; 644 | option (no_delay) = false; 645 | 646 | LogLevel level = 1; 647 | string message = 3; 648 | bool send_failed = 4; 649 | } 650 | 651 | // ==================== HOMEASSISTANT.SERVICE ==================== 652 | message SubscribeHomeassistantServicesRequest { 653 | option (id) = 34; 654 | option (source) = SOURCE_CLIENT; 655 | } 656 | 657 | message HomeassistantServiceMap { 658 | string key = 1; 659 | string value = 2; 660 | } 661 | 662 | message HomeassistantServiceResponse { 663 | option (id) = 35; 664 | option (source) = SOURCE_SERVER; 665 | option (no_delay) = true; 666 | 667 | string service = 1; 668 | repeated HomeassistantServiceMap data = 2; 669 | repeated HomeassistantServiceMap data_template = 3; 670 | repeated HomeassistantServiceMap variables = 4; 671 | bool is_event = 5; 672 | } 673 | 674 | // ==================== IMPORT HOME ASSISTANT STATES ==================== 675 | // 1. Client sends SubscribeHomeAssistantStatesRequest 676 | // 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async) 677 | // 3. Client sends HomeAssistantStateResponse for state changes. 678 | message SubscribeHomeAssistantStatesRequest { 679 | option (id) = 38; 680 | option (source) = SOURCE_CLIENT; 681 | } 682 | 683 | message SubscribeHomeAssistantStateResponse { 684 | option (id) = 39; 685 | option (source) = SOURCE_SERVER; 686 | string entity_id = 1; 687 | string attribute = 2; 688 | } 689 | 690 | message HomeAssistantStateResponse { 691 | option (id) = 40; 692 | option (source) = SOURCE_CLIENT; 693 | option (no_delay) = true; 694 | 695 | string entity_id = 1; 696 | string state = 2; 697 | string attribute = 3; 698 | } 699 | 700 | // ==================== IMPORT TIME ==================== 701 | message GetTimeRequest { 702 | option (id) = 36; 703 | option (source) = SOURCE_BOTH; 704 | } 705 | 706 | message GetTimeResponse { 707 | option (id) = 37; 708 | option (source) = SOURCE_BOTH; 709 | option (no_delay) = true; 710 | 711 | fixed32 epoch_seconds = 1; 712 | } 713 | 714 | // ==================== USER-DEFINES SERVICES ==================== 715 | enum ServiceArgType { 716 | SERVICE_ARG_TYPE_BOOL = 0; 717 | SERVICE_ARG_TYPE_INT = 1; 718 | SERVICE_ARG_TYPE_FLOAT = 2; 719 | SERVICE_ARG_TYPE_STRING = 3; 720 | SERVICE_ARG_TYPE_BOOL_ARRAY = 4; 721 | SERVICE_ARG_TYPE_INT_ARRAY = 5; 722 | SERVICE_ARG_TYPE_FLOAT_ARRAY = 6; 723 | SERVICE_ARG_TYPE_STRING_ARRAY = 7; 724 | } 725 | message ListEntitiesServicesArgument { 726 | string name = 1; 727 | ServiceArgType type = 2; 728 | } 729 | message ListEntitiesServicesResponse { 730 | option (id) = 41; 731 | option (source) = SOURCE_SERVER; 732 | 733 | string name = 1; 734 | fixed32 key = 2; 735 | repeated ListEntitiesServicesArgument args = 3; 736 | } 737 | message ExecuteServiceArgument { 738 | bool bool_ = 1; 739 | int32 legacy_int = 2; 740 | float float_ = 3; 741 | string string_ = 4; 742 | // ESPHome 1.14 (api v1.3) make int a signed value 743 | sint32 int_ = 5; 744 | repeated bool bool_array = 6 [packed=false]; 745 | repeated sint32 int_array = 7 [packed=false]; 746 | repeated float float_array = 8 [packed=false]; 747 | repeated string string_array = 9; 748 | } 749 | message ExecuteServiceRequest { 750 | option (id) = 42; 751 | option (source) = SOURCE_CLIENT; 752 | option (no_delay) = true; 753 | 754 | fixed32 key = 1; 755 | repeated ExecuteServiceArgument args = 2; 756 | } 757 | 758 | // ==================== CAMERA ==================== 759 | message ListEntitiesCameraResponse { 760 | option (id) = 43; 761 | option (source) = SOURCE_SERVER; 762 | option (ifdef) = "USE_ESP32_CAMERA"; 763 | 764 | string object_id = 1; 765 | fixed32 key = 2; 766 | string name = 3; 767 | string unique_id = 4; 768 | bool disabled_by_default = 5; 769 | string icon = 6; 770 | EntityCategory entity_category = 7; 771 | } 772 | 773 | message CameraImageResponse { 774 | option (id) = 44; 775 | option (source) = SOURCE_SERVER; 776 | option (ifdef) = "USE_ESP32_CAMERA"; 777 | 778 | fixed32 key = 1; 779 | bytes data = 2; 780 | bool done = 3; 781 | } 782 | message CameraImageRequest { 783 | option (id) = 45; 784 | option (source) = SOURCE_CLIENT; 785 | option (ifdef) = "USE_ESP32_CAMERA"; 786 | option (no_delay) = true; 787 | 788 | bool single = 1; 789 | bool stream = 2; 790 | } 791 | 792 | // ==================== CLIMATE ==================== 793 | enum ClimateMode { 794 | CLIMATE_MODE_OFF = 0; 795 | CLIMATE_MODE_HEAT_COOL = 1; 796 | CLIMATE_MODE_COOL = 2; 797 | CLIMATE_MODE_HEAT = 3; 798 | CLIMATE_MODE_FAN_ONLY = 4; 799 | CLIMATE_MODE_DRY = 5; 800 | CLIMATE_MODE_AUTO = 6; 801 | } 802 | enum ClimateFanMode { 803 | CLIMATE_FAN_ON = 0; 804 | CLIMATE_FAN_OFF = 1; 805 | CLIMATE_FAN_AUTO = 2; 806 | CLIMATE_FAN_LOW = 3; 807 | CLIMATE_FAN_MEDIUM = 4; 808 | CLIMATE_FAN_HIGH = 5; 809 | CLIMATE_FAN_MIDDLE = 6; 810 | CLIMATE_FAN_FOCUS = 7; 811 | CLIMATE_FAN_DIFFUSE = 8; 812 | CLIMATE_FAN_QUIET = 9; 813 | } 814 | enum ClimateSwingMode { 815 | CLIMATE_SWING_OFF = 0; 816 | CLIMATE_SWING_BOTH = 1; 817 | CLIMATE_SWING_VERTICAL = 2; 818 | CLIMATE_SWING_HORIZONTAL = 3; 819 | } 820 | enum ClimateAction { 821 | CLIMATE_ACTION_OFF = 0; 822 | // values same as mode for readability 823 | CLIMATE_ACTION_COOLING = 2; 824 | CLIMATE_ACTION_HEATING = 3; 825 | CLIMATE_ACTION_IDLE = 4; 826 | CLIMATE_ACTION_DRYING = 5; 827 | CLIMATE_ACTION_FAN = 6; 828 | } 829 | enum ClimatePreset { 830 | CLIMATE_PRESET_NONE = 0; 831 | CLIMATE_PRESET_HOME = 1; 832 | CLIMATE_PRESET_AWAY = 2; 833 | CLIMATE_PRESET_BOOST = 3; 834 | CLIMATE_PRESET_COMFORT = 4; 835 | CLIMATE_PRESET_ECO = 5; 836 | CLIMATE_PRESET_SLEEP = 6; 837 | CLIMATE_PRESET_ACTIVITY = 7; 838 | } 839 | message ListEntitiesClimateResponse { 840 | option (id) = 46; 841 | option (source) = SOURCE_SERVER; 842 | option (ifdef) = "USE_CLIMATE"; 843 | 844 | string object_id = 1; 845 | fixed32 key = 2; 846 | string name = 3; 847 | string unique_id = 4; 848 | 849 | bool supports_current_temperature = 5; 850 | bool supports_two_point_target_temperature = 6; 851 | repeated ClimateMode supported_modes = 7; 852 | float visual_min_temperature = 8; 853 | float visual_max_temperature = 9; 854 | float visual_target_temperature_step = 10; 855 | // for older peer versions - in new system this 856 | // is if CLIMATE_PRESET_AWAY exists is supported_presets 857 | bool legacy_supports_away = 11; 858 | bool supports_action = 12; 859 | repeated ClimateFanMode supported_fan_modes = 13; 860 | repeated ClimateSwingMode supported_swing_modes = 14; 861 | repeated string supported_custom_fan_modes = 15; 862 | repeated ClimatePreset supported_presets = 16; 863 | repeated string supported_custom_presets = 17; 864 | bool disabled_by_default = 18; 865 | string icon = 19; 866 | EntityCategory entity_category = 20; 867 | float visual_current_temperature_step = 21; 868 | bool supports_current_humidity = 22; 869 | bool supports_target_humidity = 23; 870 | float visual_min_humidity = 24; 871 | float visual_max_humidity = 25; 872 | } 873 | message ClimateStateResponse { 874 | option (id) = 47; 875 | option (source) = SOURCE_SERVER; 876 | option (ifdef) = "USE_CLIMATE"; 877 | option (no_delay) = true; 878 | 879 | fixed32 key = 1; 880 | ClimateMode mode = 2; 881 | float current_temperature = 3; 882 | float target_temperature = 4; 883 | float target_temperature_low = 5; 884 | float target_temperature_high = 6; 885 | bool unused_legacy_away = 7; 886 | ClimateAction action = 8; 887 | ClimateFanMode fan_mode = 9; 888 | ClimateSwingMode swing_mode = 10; 889 | string custom_fan_mode = 11; 890 | ClimatePreset preset = 12; 891 | string custom_preset = 13; 892 | float current_humidity = 14; 893 | float target_humidity = 15; 894 | } 895 | message ClimateCommandRequest { 896 | option (id) = 48; 897 | option (source) = SOURCE_CLIENT; 898 | option (ifdef) = "USE_CLIMATE"; 899 | option (no_delay) = true; 900 | 901 | fixed32 key = 1; 902 | bool has_mode = 2; 903 | ClimateMode mode = 3; 904 | bool has_target_temperature = 4; 905 | float target_temperature = 5; 906 | bool has_target_temperature_low = 6; 907 | float target_temperature_low = 7; 908 | bool has_target_temperature_high = 8; 909 | float target_temperature_high = 9; 910 | bool unused_has_legacy_away = 10; 911 | bool unused_legacy_away = 11; 912 | bool has_fan_mode = 12; 913 | ClimateFanMode fan_mode = 13; 914 | bool has_swing_mode = 14; 915 | ClimateSwingMode swing_mode = 15; 916 | bool has_custom_fan_mode = 16; 917 | string custom_fan_mode = 17; 918 | bool has_preset = 18; 919 | ClimatePreset preset = 19; 920 | bool has_custom_preset = 20; 921 | string custom_preset = 21; 922 | bool has_target_humidity = 22; 923 | float target_humidity = 23; 924 | } 925 | 926 | // ==================== NUMBER ==================== 927 | enum NumberMode { 928 | NUMBER_MODE_AUTO = 0; 929 | NUMBER_MODE_BOX = 1; 930 | NUMBER_MODE_SLIDER = 2; 931 | } 932 | message ListEntitiesNumberResponse { 933 | option (id) = 49; 934 | option (source) = SOURCE_SERVER; 935 | option (ifdef) = "USE_NUMBER"; 936 | 937 | string object_id = 1; 938 | fixed32 key = 2; 939 | string name = 3; 940 | string unique_id = 4; 941 | 942 | string icon = 5; 943 | float min_value = 6; 944 | float max_value = 7; 945 | float step = 8; 946 | bool disabled_by_default = 9; 947 | EntityCategory entity_category = 10; 948 | string unit_of_measurement = 11; 949 | NumberMode mode = 12; 950 | string device_class = 13; 951 | } 952 | message NumberStateResponse { 953 | option (id) = 50; 954 | option (source) = SOURCE_SERVER; 955 | option (ifdef) = "USE_NUMBER"; 956 | option (no_delay) = true; 957 | 958 | fixed32 key = 1; 959 | float state = 2; 960 | // If the number does not have a valid state yet. 961 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 962 | bool missing_state = 3; 963 | } 964 | message NumberCommandRequest { 965 | option (id) = 51; 966 | option (source) = SOURCE_CLIENT; 967 | option (ifdef) = "USE_NUMBER"; 968 | option (no_delay) = true; 969 | 970 | fixed32 key = 1; 971 | float state = 2; 972 | } 973 | 974 | // ==================== SELECT ==================== 975 | message ListEntitiesSelectResponse { 976 | option (id) = 52; 977 | option (source) = SOURCE_SERVER; 978 | option (ifdef) = "USE_SELECT"; 979 | 980 | string object_id = 1; 981 | fixed32 key = 2; 982 | string name = 3; 983 | string unique_id = 4; 984 | 985 | string icon = 5; 986 | repeated string options = 6; 987 | bool disabled_by_default = 7; 988 | EntityCategory entity_category = 8; 989 | } 990 | message SelectStateResponse { 991 | option (id) = 53; 992 | option (source) = SOURCE_SERVER; 993 | option (ifdef) = "USE_SELECT"; 994 | option (no_delay) = true; 995 | 996 | fixed32 key = 1; 997 | string state = 2; 998 | // If the select does not have a valid state yet. 999 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 1000 | bool missing_state = 3; 1001 | } 1002 | message SelectCommandRequest { 1003 | option (id) = 54; 1004 | option (source) = SOURCE_CLIENT; 1005 | option (ifdef) = "USE_SELECT"; 1006 | option (no_delay) = true; 1007 | 1008 | fixed32 key = 1; 1009 | string state = 2; 1010 | } 1011 | 1012 | 1013 | // ==================== LOCK ==================== 1014 | enum LockState { 1015 | LOCK_STATE_NONE = 0; 1016 | LOCK_STATE_LOCKED = 1; 1017 | LOCK_STATE_UNLOCKED = 2; 1018 | LOCK_STATE_JAMMED = 3; 1019 | LOCK_STATE_LOCKING = 4; 1020 | LOCK_STATE_UNLOCKING = 5; 1021 | } 1022 | enum LockCommand { 1023 | LOCK_UNLOCK = 0; 1024 | LOCK_LOCK = 1; 1025 | LOCK_OPEN = 2; 1026 | } 1027 | message ListEntitiesLockResponse { 1028 | option (id) = 58; 1029 | option (source) = SOURCE_SERVER; 1030 | option (ifdef) = "USE_LOCK"; 1031 | 1032 | string object_id = 1; 1033 | fixed32 key = 2; 1034 | string name = 3; 1035 | string unique_id = 4; 1036 | 1037 | string icon = 5; 1038 | bool disabled_by_default = 6; 1039 | EntityCategory entity_category = 7; 1040 | bool assumed_state = 8; 1041 | 1042 | bool supports_open = 9; 1043 | bool requires_code = 10; 1044 | 1045 | // Not yet implemented: 1046 | string code_format = 11; 1047 | } 1048 | message LockStateResponse { 1049 | option (id) = 59; 1050 | option (source) = SOURCE_SERVER; 1051 | option (ifdef) = "USE_LOCK"; 1052 | option (no_delay) = true; 1053 | fixed32 key = 1; 1054 | LockState state = 2; 1055 | } 1056 | message LockCommandRequest { 1057 | option (id) = 60; 1058 | option (source) = SOURCE_CLIENT; 1059 | option (ifdef) = "USE_LOCK"; 1060 | option (no_delay) = true; 1061 | fixed32 key = 1; 1062 | LockCommand command = 2; 1063 | 1064 | // Not yet implemented: 1065 | bool has_code = 3; 1066 | string code = 4; 1067 | } 1068 | 1069 | // ==================== BUTTON ==================== 1070 | message ListEntitiesButtonResponse { 1071 | option (id) = 61; 1072 | option (source) = SOURCE_SERVER; 1073 | option (ifdef) = "USE_BUTTON"; 1074 | 1075 | string object_id = 1; 1076 | fixed32 key = 2; 1077 | string name = 3; 1078 | string unique_id = 4; 1079 | 1080 | string icon = 5; 1081 | bool disabled_by_default = 6; 1082 | EntityCategory entity_category = 7; 1083 | string device_class = 8; 1084 | } 1085 | message ButtonCommandRequest { 1086 | option (id) = 62; 1087 | option (source) = SOURCE_CLIENT; 1088 | option (ifdef) = "USE_BUTTON"; 1089 | option (no_delay) = true; 1090 | 1091 | fixed32 key = 1; 1092 | } 1093 | 1094 | // ==================== MEDIA PLAYER ==================== 1095 | enum MediaPlayerState { 1096 | MEDIA_PLAYER_STATE_NONE = 0; 1097 | MEDIA_PLAYER_STATE_IDLE = 1; 1098 | MEDIA_PLAYER_STATE_PLAYING = 2; 1099 | MEDIA_PLAYER_STATE_PAUSED = 3; 1100 | } 1101 | enum MediaPlayerCommand { 1102 | MEDIA_PLAYER_COMMAND_PLAY = 0; 1103 | MEDIA_PLAYER_COMMAND_PAUSE = 1; 1104 | MEDIA_PLAYER_COMMAND_STOP = 2; 1105 | MEDIA_PLAYER_COMMAND_MUTE = 3; 1106 | MEDIA_PLAYER_COMMAND_UNMUTE = 4; 1107 | } 1108 | message ListEntitiesMediaPlayerResponse { 1109 | option (id) = 63; 1110 | option (source) = SOURCE_SERVER; 1111 | option (ifdef) = "USE_MEDIA_PLAYER"; 1112 | 1113 | string object_id = 1; 1114 | fixed32 key = 2; 1115 | string name = 3; 1116 | string unique_id = 4; 1117 | 1118 | string icon = 5; 1119 | bool disabled_by_default = 6; 1120 | EntityCategory entity_category = 7; 1121 | 1122 | bool supports_pause = 8; 1123 | } 1124 | message MediaPlayerStateResponse { 1125 | option (id) = 64; 1126 | option (source) = SOURCE_SERVER; 1127 | option (ifdef) = "USE_MEDIA_PLAYER"; 1128 | option (no_delay) = true; 1129 | fixed32 key = 1; 1130 | MediaPlayerState state = 2; 1131 | float volume = 3; 1132 | bool muted = 4; 1133 | } 1134 | message MediaPlayerCommandRequest { 1135 | option (id) = 65; 1136 | option (source) = SOURCE_CLIENT; 1137 | option (ifdef) = "USE_MEDIA_PLAYER"; 1138 | option (no_delay) = true; 1139 | 1140 | fixed32 key = 1; 1141 | 1142 | bool has_command = 2; 1143 | MediaPlayerCommand command = 3; 1144 | 1145 | bool has_volume = 4; 1146 | float volume = 5; 1147 | 1148 | bool has_media_url = 6; 1149 | string media_url = 7; 1150 | 1151 | bool has_announcement = 8; 1152 | bool announcement = 9; 1153 | } 1154 | 1155 | // ==================== BLUETOOTH ==================== 1156 | message SubscribeBluetoothLEAdvertisementsRequest { 1157 | option (id) = 66; 1158 | option (source) = SOURCE_CLIENT; 1159 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1160 | 1161 | uint32 flags = 1; 1162 | } 1163 | 1164 | message BluetoothServiceData { 1165 | string uuid = 1; 1166 | repeated uint32 legacy_data = 2 [deprecated = true]; 1167 | bytes data = 3; // Changed in proto version 1.7 1168 | } 1169 | message BluetoothLEAdvertisementResponse { 1170 | option (id) = 67; 1171 | option (source) = SOURCE_SERVER; 1172 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1173 | option (no_delay) = true; 1174 | 1175 | uint64 address = 1; 1176 | string name = 2; 1177 | sint32 rssi = 3; 1178 | 1179 | repeated string service_uuids = 4; 1180 | repeated BluetoothServiceData service_data = 5; 1181 | repeated BluetoothServiceData manufacturer_data = 6; 1182 | 1183 | uint32 address_type = 7; 1184 | } 1185 | 1186 | message BluetoothLERawAdvertisement { 1187 | uint64 address = 1; 1188 | sint32 rssi = 2; 1189 | uint32 address_type = 3; 1190 | 1191 | bytes data = 4; 1192 | } 1193 | 1194 | message BluetoothLERawAdvertisementsResponse { 1195 | option (id) = 93; 1196 | option (source) = SOURCE_SERVER; 1197 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1198 | option (no_delay) = true; 1199 | 1200 | repeated BluetoothLERawAdvertisement advertisements = 1; 1201 | } 1202 | 1203 | enum BluetoothDeviceRequestType { 1204 | BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; 1205 | BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; 1206 | BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2; 1207 | BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3; 1208 | BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE = 4; 1209 | BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE = 5; 1210 | BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE = 6; 1211 | } 1212 | 1213 | message BluetoothDeviceRequest { 1214 | option (id) = 68; 1215 | option (source) = SOURCE_CLIENT; 1216 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1217 | 1218 | uint64 address = 1; 1219 | BluetoothDeviceRequestType request_type = 2; 1220 | bool has_address_type = 3; 1221 | uint32 address_type = 4; 1222 | } 1223 | 1224 | message BluetoothDeviceConnectionResponse { 1225 | option (id) = 69; 1226 | option (source) = SOURCE_SERVER; 1227 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1228 | 1229 | uint64 address = 1; 1230 | bool connected = 2; 1231 | uint32 mtu = 3; 1232 | int32 error = 4; 1233 | } 1234 | 1235 | message BluetoothGATTGetServicesRequest { 1236 | option (id) = 70; 1237 | option (source) = SOURCE_CLIENT; 1238 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1239 | 1240 | uint64 address = 1; 1241 | } 1242 | 1243 | message BluetoothGATTDescriptor { 1244 | repeated uint64 uuid = 1; 1245 | uint32 handle = 2; 1246 | } 1247 | 1248 | message BluetoothGATTCharacteristic { 1249 | repeated uint64 uuid = 1; 1250 | uint32 handle = 2; 1251 | uint32 properties = 3; 1252 | repeated BluetoothGATTDescriptor descriptors = 4; 1253 | } 1254 | 1255 | message BluetoothGATTService { 1256 | repeated uint64 uuid = 1; 1257 | uint32 handle = 2; 1258 | repeated BluetoothGATTCharacteristic characteristics = 3; 1259 | } 1260 | 1261 | message BluetoothGATTGetServicesResponse { 1262 | option (id) = 71; 1263 | option (source) = SOURCE_SERVER; 1264 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1265 | 1266 | uint64 address = 1; 1267 | repeated BluetoothGATTService services = 2; 1268 | } 1269 | 1270 | message BluetoothGATTGetServicesDoneResponse { 1271 | option (id) = 72; 1272 | option (source) = SOURCE_SERVER; 1273 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1274 | 1275 | uint64 address = 1; 1276 | } 1277 | 1278 | message BluetoothGATTReadRequest { 1279 | option (id) = 73; 1280 | option (source) = SOURCE_CLIENT; 1281 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1282 | 1283 | uint64 address = 1; 1284 | uint32 handle = 2; 1285 | } 1286 | 1287 | message BluetoothGATTReadResponse { 1288 | option (id) = 74; 1289 | option (source) = SOURCE_SERVER; 1290 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1291 | 1292 | uint64 address = 1; 1293 | uint32 handle = 2; 1294 | 1295 | bytes data = 3; 1296 | 1297 | } 1298 | 1299 | message BluetoothGATTWriteRequest { 1300 | option (id) = 75; 1301 | option (source) = SOURCE_CLIENT; 1302 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1303 | 1304 | uint64 address = 1; 1305 | uint32 handle = 2; 1306 | bool response = 3; 1307 | 1308 | bytes data = 4; 1309 | } 1310 | 1311 | message BluetoothGATTReadDescriptorRequest { 1312 | option (id) = 76; 1313 | option (source) = SOURCE_CLIENT; 1314 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1315 | 1316 | uint64 address = 1; 1317 | uint32 handle = 2; 1318 | } 1319 | 1320 | message BluetoothGATTWriteDescriptorRequest { 1321 | option (id) = 77; 1322 | option (source) = SOURCE_CLIENT; 1323 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1324 | 1325 | uint64 address = 1; 1326 | uint32 handle = 2; 1327 | 1328 | bytes data = 3; 1329 | } 1330 | 1331 | message BluetoothGATTNotifyRequest { 1332 | option (id) = 78; 1333 | option (source) = SOURCE_CLIENT; 1334 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1335 | 1336 | uint64 address = 1; 1337 | uint32 handle = 2; 1338 | bool enable = 3; 1339 | } 1340 | 1341 | message BluetoothGATTNotifyDataResponse { 1342 | option (id) = 79; 1343 | option (source) = SOURCE_SERVER; 1344 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1345 | 1346 | uint64 address = 1; 1347 | uint32 handle = 2; 1348 | 1349 | bytes data = 3; 1350 | } 1351 | 1352 | message SubscribeBluetoothConnectionsFreeRequest { 1353 | option (id) = 80; 1354 | option (source) = SOURCE_CLIENT; 1355 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1356 | } 1357 | 1358 | message BluetoothConnectionsFreeResponse { 1359 | option (id) = 81; 1360 | option (source) = SOURCE_SERVER; 1361 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1362 | 1363 | uint32 free = 1; 1364 | uint32 limit = 2; 1365 | } 1366 | 1367 | message BluetoothGATTErrorResponse { 1368 | option (id) = 82; 1369 | option (source) = SOURCE_SERVER; 1370 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1371 | 1372 | uint64 address = 1; 1373 | uint32 handle = 2; 1374 | int32 error = 3; 1375 | } 1376 | 1377 | message BluetoothGATTWriteResponse { 1378 | option (id) = 83; 1379 | option (source) = SOURCE_SERVER; 1380 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1381 | 1382 | uint64 address = 1; 1383 | uint32 handle = 2; 1384 | } 1385 | 1386 | message BluetoothGATTNotifyResponse { 1387 | option (id) = 84; 1388 | option (source) = SOURCE_SERVER; 1389 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1390 | 1391 | uint64 address = 1; 1392 | uint32 handle = 2; 1393 | } 1394 | 1395 | message BluetoothDevicePairingResponse { 1396 | option (id) = 85; 1397 | option (source) = SOURCE_SERVER; 1398 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1399 | 1400 | uint64 address = 1; 1401 | bool paired = 2; 1402 | int32 error = 3; 1403 | } 1404 | 1405 | message BluetoothDeviceUnpairingResponse { 1406 | option (id) = 86; 1407 | option (source) = SOURCE_SERVER; 1408 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1409 | 1410 | uint64 address = 1; 1411 | bool success = 2; 1412 | int32 error = 3; 1413 | } 1414 | 1415 | message UnsubscribeBluetoothLEAdvertisementsRequest { 1416 | option (id) = 87; 1417 | option (source) = SOURCE_CLIENT; 1418 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1419 | } 1420 | 1421 | message BluetoothDeviceClearCacheResponse { 1422 | option (id) = 88; 1423 | option (source) = SOURCE_SERVER; 1424 | option (ifdef) = "USE_BLUETOOTH_PROXY"; 1425 | 1426 | uint64 address = 1; 1427 | bool success = 2; 1428 | int32 error = 3; 1429 | } 1430 | 1431 | // ==================== PUSH TO TALK ==================== 1432 | enum VoiceAssistantSubscribeFlag { 1433 | VOICE_ASSISTANT_SUBSCRIBE_NONE = 0; 1434 | VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1; 1435 | } 1436 | 1437 | message SubscribeVoiceAssistantRequest { 1438 | option (id) = 89; 1439 | option (source) = SOURCE_CLIENT; 1440 | option (ifdef) = "USE_VOICE_ASSISTANT"; 1441 | 1442 | bool subscribe = 1; 1443 | uint32 flags = 2; 1444 | } 1445 | 1446 | enum VoiceAssistantRequestFlag { 1447 | VOICE_ASSISTANT_REQUEST_NONE = 0; 1448 | VOICE_ASSISTANT_REQUEST_USE_VAD = 1; 1449 | VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD = 2; 1450 | } 1451 | 1452 | message VoiceAssistantAudioSettings { 1453 | uint32 noise_suppression_level = 1; 1454 | uint32 auto_gain = 2; 1455 | float volume_multiplier = 3; 1456 | } 1457 | 1458 | message VoiceAssistantRequest { 1459 | option (id) = 90; 1460 | option (source) = SOURCE_SERVER; 1461 | option (ifdef) = "USE_VOICE_ASSISTANT"; 1462 | 1463 | bool start = 1; 1464 | string conversation_id = 2; 1465 | uint32 flags = 3; 1466 | VoiceAssistantAudioSettings audio_settings = 4; 1467 | string wake_word_phrase = 5; 1468 | } 1469 | 1470 | message VoiceAssistantResponse { 1471 | option (id) = 91; 1472 | option (source) = SOURCE_CLIENT; 1473 | option (ifdef) = "USE_VOICE_ASSISTANT"; 1474 | 1475 | uint32 port = 1; 1476 | bool error = 2; 1477 | } 1478 | 1479 | enum VoiceAssistantEvent { 1480 | VOICE_ASSISTANT_ERROR = 0; 1481 | VOICE_ASSISTANT_RUN_START = 1; 1482 | VOICE_ASSISTANT_RUN_END = 2; 1483 | VOICE_ASSISTANT_STT_START = 3; 1484 | VOICE_ASSISTANT_STT_END = 4; 1485 | VOICE_ASSISTANT_INTENT_START = 5; 1486 | VOICE_ASSISTANT_INTENT_END = 6; 1487 | VOICE_ASSISTANT_TTS_START = 7; 1488 | VOICE_ASSISTANT_TTS_END = 8; 1489 | VOICE_ASSISTANT_WAKE_WORD_START = 9; 1490 | VOICE_ASSISTANT_WAKE_WORD_END = 10; 1491 | VOICE_ASSISTANT_STT_VAD_START = 11; 1492 | VOICE_ASSISTANT_STT_VAD_END = 12; 1493 | VOICE_ASSISTANT_TTS_STREAM_START = 98; 1494 | VOICE_ASSISTANT_TTS_STREAM_END = 99; 1495 | } 1496 | 1497 | message VoiceAssistantEventData { 1498 | string name = 1; 1499 | string value = 2; 1500 | } 1501 | 1502 | message VoiceAssistantEventResponse { 1503 | option (id) = 92; 1504 | option (source) = SOURCE_CLIENT; 1505 | option (ifdef) = "USE_VOICE_ASSISTANT"; 1506 | 1507 | VoiceAssistantEvent event_type = 1; 1508 | repeated VoiceAssistantEventData data = 2; 1509 | } 1510 | 1511 | message VoiceAssistantAudio { 1512 | option (id) = 106; 1513 | option (source) = SOURCE_BOTH; 1514 | option (ifdef) = "USE_VOICE_ASSISTANT"; 1515 | 1516 | bytes data = 1; 1517 | bool end = 2; 1518 | } 1519 | 1520 | 1521 | // ==================== ALARM CONTROL PANEL ==================== 1522 | enum AlarmControlPanelState { 1523 | ALARM_STATE_DISARMED = 0; 1524 | ALARM_STATE_ARMED_HOME = 1; 1525 | ALARM_STATE_ARMED_AWAY = 2; 1526 | ALARM_STATE_ARMED_NIGHT = 3; 1527 | ALARM_STATE_ARMED_VACATION = 4; 1528 | ALARM_STATE_ARMED_CUSTOM_BYPASS = 5; 1529 | ALARM_STATE_PENDING = 6; 1530 | ALARM_STATE_ARMING = 7; 1531 | ALARM_STATE_DISARMING = 8; 1532 | ALARM_STATE_TRIGGERED = 9; 1533 | } 1534 | 1535 | enum AlarmControlPanelStateCommand { 1536 | ALARM_CONTROL_PANEL_DISARM = 0; 1537 | ALARM_CONTROL_PANEL_ARM_AWAY = 1; 1538 | ALARM_CONTROL_PANEL_ARM_HOME = 2; 1539 | ALARM_CONTROL_PANEL_ARM_NIGHT = 3; 1540 | ALARM_CONTROL_PANEL_ARM_VACATION = 4; 1541 | ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5; 1542 | ALARM_CONTROL_PANEL_TRIGGER = 6; 1543 | } 1544 | 1545 | message ListEntitiesAlarmControlPanelResponse { 1546 | option (id) = 94; 1547 | option (source) = SOURCE_SERVER; 1548 | option (ifdef) = "USE_ALARM_CONTROL_PANEL"; 1549 | 1550 | string object_id = 1; 1551 | fixed32 key = 2; 1552 | string name = 3; 1553 | string unique_id = 4; 1554 | string icon = 5; 1555 | bool disabled_by_default = 6; 1556 | EntityCategory entity_category = 7; 1557 | uint32 supported_features = 8; 1558 | bool requires_code = 9; 1559 | bool requires_code_to_arm = 10; 1560 | } 1561 | 1562 | message AlarmControlPanelStateResponse { 1563 | option (id) = 95; 1564 | option (source) = SOURCE_SERVER; 1565 | option (ifdef) = "USE_ALARM_CONTROL_PANEL"; 1566 | option (no_delay) = true; 1567 | fixed32 key = 1; 1568 | AlarmControlPanelState state = 2; 1569 | } 1570 | 1571 | message AlarmControlPanelCommandRequest { 1572 | option (id) = 96; 1573 | option (source) = SOURCE_CLIENT; 1574 | option (ifdef) = "USE_ALARM_CONTROL_PANEL"; 1575 | option (no_delay) = true; 1576 | fixed32 key = 1; 1577 | AlarmControlPanelStateCommand command = 2; 1578 | string code = 3; 1579 | } 1580 | 1581 | // ===================== TEXT ===================== 1582 | enum TextMode { 1583 | TEXT_MODE_TEXT = 0; 1584 | TEXT_MODE_PASSWORD = 1; 1585 | } 1586 | message ListEntitiesTextResponse { 1587 | option (id) = 97; 1588 | option (source) = SOURCE_SERVER; 1589 | option (ifdef) = "USE_TEXT"; 1590 | 1591 | string object_id = 1; 1592 | fixed32 key = 2; 1593 | string name = 3; 1594 | string unique_id = 4; 1595 | string icon = 5; 1596 | bool disabled_by_default = 6; 1597 | EntityCategory entity_category = 7; 1598 | 1599 | uint32 min_length = 8; 1600 | uint32 max_length = 9; 1601 | string pattern = 10; 1602 | TextMode mode = 11; 1603 | } 1604 | message TextStateResponse { 1605 | option (id) = 98; 1606 | option (source) = SOURCE_SERVER; 1607 | option (ifdef) = "USE_TEXT"; 1608 | option (no_delay) = true; 1609 | 1610 | fixed32 key = 1; 1611 | string state = 2; 1612 | // If the Text does not have a valid state yet. 1613 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 1614 | bool missing_state = 3; 1615 | } 1616 | message TextCommandRequest { 1617 | option (id) = 99; 1618 | option (source) = SOURCE_CLIENT; 1619 | option (ifdef) = "USE_TEXT"; 1620 | option (no_delay) = true; 1621 | 1622 | fixed32 key = 1; 1623 | string state = 2; 1624 | } 1625 | 1626 | 1627 | // ==================== DATETIME DATE ==================== 1628 | message ListEntitiesDateResponse { 1629 | option (id) = 100; 1630 | option (source) = SOURCE_SERVER; 1631 | option (ifdef) = "USE_DATETIME_DATE"; 1632 | 1633 | string object_id = 1; 1634 | fixed32 key = 2; 1635 | string name = 3; 1636 | string unique_id = 4; 1637 | 1638 | string icon = 5; 1639 | bool disabled_by_default = 6; 1640 | EntityCategory entity_category = 7; 1641 | } 1642 | message DateStateResponse { 1643 | option (id) = 101; 1644 | option (source) = SOURCE_SERVER; 1645 | option (ifdef) = "USE_DATETIME_DATE"; 1646 | option (no_delay) = true; 1647 | 1648 | fixed32 key = 1; 1649 | // If the date does not have a valid state yet. 1650 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 1651 | bool missing_state = 2; 1652 | uint32 year = 3; 1653 | uint32 month = 4; 1654 | uint32 day = 5; 1655 | } 1656 | message DateCommandRequest { 1657 | option (id) = 102; 1658 | option (source) = SOURCE_CLIENT; 1659 | option (ifdef) = "USE_DATETIME_DATE"; 1660 | option (no_delay) = true; 1661 | 1662 | fixed32 key = 1; 1663 | uint32 year = 2; 1664 | uint32 month = 3; 1665 | uint32 day = 4; 1666 | } 1667 | 1668 | // ==================== DATETIME TIME ==================== 1669 | message ListEntitiesTimeResponse { 1670 | option (id) = 103; 1671 | option (source) = SOURCE_SERVER; 1672 | option (ifdef) = "USE_DATETIME_TIME"; 1673 | 1674 | string object_id = 1; 1675 | fixed32 key = 2; 1676 | string name = 3; 1677 | string unique_id = 4; 1678 | 1679 | string icon = 5; 1680 | bool disabled_by_default = 6; 1681 | EntityCategory entity_category = 7; 1682 | } 1683 | message TimeStateResponse { 1684 | option (id) = 104; 1685 | option (source) = SOURCE_SERVER; 1686 | option (ifdef) = "USE_DATETIME_TIME"; 1687 | option (no_delay) = true; 1688 | 1689 | fixed32 key = 1; 1690 | // If the time does not have a valid state yet. 1691 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 1692 | bool missing_state = 2; 1693 | uint32 hour = 3; 1694 | uint32 minute = 4; 1695 | uint32 second = 5; 1696 | } 1697 | message TimeCommandRequest { 1698 | option (id) = 105; 1699 | option (source) = SOURCE_CLIENT; 1700 | option (ifdef) = "USE_DATETIME_TIME"; 1701 | option (no_delay) = true; 1702 | 1703 | fixed32 key = 1; 1704 | uint32 hour = 2; 1705 | uint32 minute = 3; 1706 | uint32 second = 4; 1707 | } 1708 | 1709 | // ==================== EVENT ==================== 1710 | message ListEntitiesEventResponse { 1711 | option (id) = 107; 1712 | option (source) = SOURCE_SERVER; 1713 | option (ifdef) = "USE_EVENT"; 1714 | 1715 | string object_id = 1; 1716 | fixed32 key = 2; 1717 | string name = 3; 1718 | string unique_id = 4; 1719 | 1720 | string icon = 5; 1721 | bool disabled_by_default = 6; 1722 | EntityCategory entity_category = 7; 1723 | string device_class = 8; 1724 | 1725 | repeated string event_types = 9; 1726 | } 1727 | message EventResponse { 1728 | option (id) = 108; 1729 | option (source) = SOURCE_SERVER; 1730 | option (ifdef) = "USE_EVENT"; 1731 | 1732 | fixed32 key = 1; 1733 | string event_type = 2; 1734 | } 1735 | 1736 | // ==================== VALVE ==================== 1737 | message ListEntitiesValveResponse { 1738 | option (id) = 109; 1739 | option (source) = SOURCE_SERVER; 1740 | option (ifdef) = "USE_VALVE"; 1741 | 1742 | string object_id = 1; 1743 | fixed32 key = 2; 1744 | string name = 3; 1745 | string unique_id = 4; 1746 | 1747 | string icon = 5; 1748 | bool disabled_by_default = 6; 1749 | EntityCategory entity_category = 7; 1750 | string device_class = 8; 1751 | 1752 | bool assumed_state = 9; 1753 | bool supports_position = 10; 1754 | bool supports_stop = 11; 1755 | } 1756 | 1757 | enum ValveOperation { 1758 | VALVE_OPERATION_IDLE = 0; 1759 | VALVE_OPERATION_IS_OPENING = 1; 1760 | VALVE_OPERATION_IS_CLOSING = 2; 1761 | } 1762 | message ValveStateResponse { 1763 | option (id) = 110; 1764 | option (source) = SOURCE_SERVER; 1765 | option (ifdef) = "USE_VALVE"; 1766 | option (no_delay) = true; 1767 | 1768 | fixed32 key = 1; 1769 | float position = 2; 1770 | ValveOperation current_operation = 3; 1771 | } 1772 | 1773 | message ValveCommandRequest { 1774 | option (id) = 111; 1775 | option (source) = SOURCE_CLIENT; 1776 | option (ifdef) = "USE_VALVE"; 1777 | option (no_delay) = true; 1778 | 1779 | fixed32 key = 1; 1780 | bool has_position = 2; 1781 | float position = 3; 1782 | bool stop = 4; 1783 | } 1784 | 1785 | // ==================== DATETIME DATETIME ==================== 1786 | message ListEntitiesDateTimeResponse { 1787 | option (id) = 112; 1788 | option (source) = SOURCE_SERVER; 1789 | option (ifdef) = "USE_DATETIME_DATETIME"; 1790 | 1791 | string object_id = 1; 1792 | fixed32 key = 2; 1793 | string name = 3; 1794 | string unique_id = 4; 1795 | 1796 | string icon = 5; 1797 | bool disabled_by_default = 6; 1798 | EntityCategory entity_category = 7; 1799 | } 1800 | message DateTimeStateResponse { 1801 | option (id) = 113; 1802 | option (source) = SOURCE_SERVER; 1803 | option (ifdef) = "USE_DATETIME_DATETIME"; 1804 | option (no_delay) = true; 1805 | 1806 | fixed32 key = 1; 1807 | // If the datetime does not have a valid state yet. 1808 | // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller 1809 | bool missing_state = 2; 1810 | fixed32 epoch_seconds = 3; 1811 | } 1812 | message DateTimeCommandRequest { 1813 | option (id) = 114; 1814 | option (source) = SOURCE_CLIENT; 1815 | option (ifdef) = "USE_DATETIME_DATETIME"; 1816 | option (no_delay) = true; 1817 | 1818 | fixed32 key = 1; 1819 | fixed32 epoch_seconds = 2; 1820 | } --------------------------------------------------------------------------------