Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions .github/workflows/lint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ jobs:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
- name: Checkout
uses: actions/checkout@v4
with:
go-version: ^1.21
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: golangci-lint
uses: golangci/golangci-lint-action@v2
uses: golangci/golangci-lint-action@v9
with:
version: latest
skip-go-installation: true
# Optional: show only new issues if it's a pull request. The default value is `false`.
# only-new-issues: true
11 changes: 6 additions & 5 deletions .github/workflows/publish_executables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ jobs:

steps:
- name: checkout the source code
uses: actions/checkout@v2
uses: actions/checkout@v4

- uses: actions/setup-go@v2
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ^1.21
go-version-file: go.mod

- name: Cache go modules
uses: actions/cache@v2
uses: actions/cache@v6
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
Expand All @@ -37,7 +38,7 @@ jobs:

- name: update release notes and executables
if: startsWith(github.ref, 'refs/tags/') # executes only for new release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/esphome_config
/.esphome/
/secrets.yaml
esphomectl
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,56 @@
## Changes by Version

### v1.4.0 (2026-07-18)

Proto baseline jumped from ESPHome **2024.5.0** (library snapshot 2024-06-15) to
[**2026.7.0**](https://github.com/esphome/esphome/releases/tag/2026.7.0).
Full migration notes: [docs/API_PROTO_UPDATE_2024.5.0_to_2026.7.0.md](docs/API_PROTO_UPDATE_2024.5.0_to_2026.7.0.md).

**Contains BREAKING CHANGES** (see below and the doc above).

#### Protocol and generated API
* Sync `api.proto` / `api_options.proto` with ESPHome **2026.7.0**
* Regenerate `pkg/api` protobuf bindings; expand `helper.go` to all **148** message type IDs
(was 111; highest ID 114 → 148)
* New entities/features on the wire: siren, update, water heater, infrared, serial proxy,
radio frequency, Z-Wave proxy, noise key provisioning, HA action / service responses,
Bluetooth scanner mode and connection params, voice assistant extensions

#### Client (`pkg/client`, `pkg/types`)
* Advertise Hello API version **1.14**
* Session setup: use **`Hello()`** on modern devices (ESPHome 2026.1+)
* **`Login(password)`** kept for older firmware only; password auth was **removed** in
ESPHome 2026.1.0. Prefer Noise encryption via `encryptionKey` on `GetClient`
* `Login` treats auth-response timeout as success after Hello (modern devices ignore password)
* Answer device-originated `GetTimeRequest` (timezone-aware epoch seconds)
* Record `DisconnectReason` from device `DisconnectRequest`
* Safer shutdown (`close`-once + deadline) and register waiters before send
* `DeviceInfo` gains friendly name, manufacturer, project, BT MAC, encryption flags,
areas/devices, Z-Wave, serial proxies, and related fields
* `GetLogEntry`: decode log `message` from `bytes`; `SendFailed` always false (field removed)

#### CLI (`esphomectl`) and examples
* Always complete session setup: `Hello()`, or legacy `Login(password)` when a password is set
* Warn on stderr when API password is used (deprecated; removed in ESPHome 2026.1.0)
* `--password` help/examples mark password auth as removed; prefer `--encryption-key`
* Persist expanded device info after login; entity list UI no longer uses removed `unique_id`
* Examples follow the same Hello / Login session pattern

#### Build and docs
* Release builds use `-trimpath` and slim ldflags (`-s -w`); README documents the same for local CLI builds
* Migration guide: `docs/API_PROTO_UPDATE_2024.5.0_to_2026.7.0.md`
* README / `proto/README.md` updated for 2026.7.0 and encryption-first setup

#### Breaking changes
* `api.ConnectRequest` / `api.ConnectResponse` → `api.AuthenticationRequest` / `api.AuthenticationResponse`
(IDs 3/4; not processed by ESPHome 2026.1.0+)
* `api.HomeassistantServiceResponse` → `api.HomeassistantActionRequest`
* Entity list messages: `unique_id` removed (use `object_id` + `key`, optional `device_id`)
* `SubscribeLogsResponse.message`: `string` → `bytes`; `send_failed` removed
* `BluetoothLEAdvertisementResponse.name`: `string` → `bytes`
* Bluetooth helper constants use `*TypeID` suffix (aligned with other message IDs)
* Callers must run `Hello()` (or deprecated `Login`) after `GetClient` for a full session

### v1.3.0 (2023-01-06)
* update proto: bluetooth support included ([#7](https://github.com/mycontroller-org/esphome_api/pull/7), [@jkandasa](https://github.com/jkandasa))
* support encrypted connection ([#5](https://github.com/mycontroller-org/esphome_api/pull/5), [@jkandasa](https://github.com/jkandasa))
Expand Down
41 changes: 29 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,22 @@ commands, subscribing to state updates, and receiving device information.

## Installation

To install the `esphome_api` library, use the following command:
Library:

```bash
go build -o esphome ./cli/main.go
go get github.com/mycontroller-org/esphome_api@latest
```

CLI (`esphomectl`):

```bash
go build -trimpath -ldflags "-s -w" -o esphomectl ./cli/main.go
```

Release binaries (multi-platform, version ldflags):

```bash
./scripts/generate_executables.sh
```

## Usage
Expand Down Expand Up @@ -47,16 +59,20 @@ if err != nil {
defer client.Close()
```

### 3. Connect and Authenticate (if required)
### 3. Hello

```go
// If your device requires authentication, log in with the password
password := "YOUR_PASSWORD"
if err := client.Login(password); err != nil {
// Required after GetClient on modern devices.
if _, err := client.Hello(); err != nil {
log.Fatalln(err)
}

// Deprecated (pre-2026.1 password auth only):
// if err := client.Login("YOUR_PASSWORD"); err != nil { ... }
```

See [docs/API_PROTO_UPDATE_2024.5.0_to_2026.7.0.md](docs/API_PROTO_UPDATE_2024.5.0_to_2026.7.0.md) for protocol changes and breaking changes.

### 4. Send Commands and Receive State Updates

```go
Expand Down Expand Up @@ -105,21 +121,22 @@ tool, which utilizes the `esphome_api` library:
active: esphome.local:6053
devices:
- address: esphome.local:6053
password: BASE64/YOUR_ENCODED_PASSWORD
encryptionKey: YOUR_ENCRYPTION_KEY
timeout: 10s
info:
name: My ESPHome Device
model: NodeMCU
macAddress: AC:BC:32:89:0E:A9
esphomeVersion: "1.15.0"
compilationTime: "2023-10-26T10:00:00"
usesPassword: true
esphomeVersion: "2026.7.0"
compilationTime: "2026-01-15T10:00:00"
usesPassword: false
hasDeepSleep: false
statusOn: 2023-10-26T12:00:00+05:30
apiEncryptionSupported: true
statusOn: 2026-07-18T12:00:00+05:30
```

**Note:** The password is encoded in Base64 format. You can encode your password using the following command:
**Note:** Prefer `encryptionKey`. API password auth was removed in ESPHome 2026.1.0.
If you still need a password for older firmware, store it Base64-encoded:

```bash
echo -n "YOUR_PASSWORD" | base64
Expand Down
10 changes: 5 additions & 5 deletions cli/command/device/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,21 +27,21 @@ var deviceContextCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
if rootCmd.CONFIG.Active == "" {
fmt.Fprintln(cmd.OutOrStdout(), "No resource found")
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No resource found")
return
}
fmt.Fprintf(cmd.ErrOrStderr(), "Active node '%s'\n", rootCmd.CONFIG.Active)
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "Active node '%s'\n", rootCmd.CONFIG.Active)
return
}
rootCmd.CONFIG.Active = strings.TrimSpace(args[0])
client, err := rootCmd.GetActiveClient(nil)
if err != nil {
fmt.Fprintln(cmd.ErrOrStderr(), "Error on login", err)
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "Error on login", err)
return
}
if client != nil {
rootCmd.WriteConfigFile()
fmt.Fprintf(cmd.OutOrStdout(), "Switched to '%s'\n", rootCmd.CONFIG.Active)
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "Switched to '%s'\n", rootCmd.CONFIG.Active)
}
},
}
Expand All @@ -54,7 +54,7 @@ var getDevicesCmd = &cobra.Command{
`,
Run: func(cmd *cobra.Command, args []string) {
if len(rootCmd.CONFIG.Devices) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), "No resource found")
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No resource found")
return
}
headers := []printer.Header{
Expand Down
16 changes: 8 additions & 8 deletions cli/command/device/get_entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ var getEntitiesCmd = &cobra.Command{
default:
_, _deviceClass, err := filterUtils.GetValueByKeyPath(entity, "deviceClass")
if err != nil {
fmt.Fprintln(cmd.ErrOrStderr(), "error:", err)
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "error:", err)
return
}
deviceClass := convertor.ToString(_deviceClass)
Expand All @@ -60,13 +60,13 @@ var getEntitiesCmd = &cobra.Command{

client, err := rootCmd.GetActiveClient(collectEntities)
if err != nil {
fmt.Fprintln(cmd.ErrOrStderr(), "error:", err.Error())
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "error:", err.Error())
return
}

err = client.ListEntities()
if err != nil {
fmt.Fprintln(cmd.ErrOrStderr(), "error:", err.Error())
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), "error:", err.Error())
return
}

Expand All @@ -79,21 +79,21 @@ var getEntitiesCmd = &cobra.Command{
}

if len(entities) == 0 {
fmt.Fprintln(cmd.OutOrStdout(), "No resource found")
_, _ = fmt.Fprintln(cmd.OutOrStdout(), "No resource found")
return
}

for k, _sensors := range entities {
fmt.Fprintln(cmd.OutOrStdout())
fmt.Fprintln(cmd.OutOrStdout(), strings.ToUpper(k))
_, _ = fmt.Fprintln(cmd.OutOrStdout())
_, _ = fmt.Fprintln(cmd.OutOrStdout(), strings.ToUpper(k))

switch k {
case "light":
headers := []printer.Header{
{Title: "name", ValuePath: "name"},
{Title: "object id", ValuePath: "objectId"},
{Title: "key", ValuePath: "key"},
{Title: "unique id", ValuePath: "uniqueId"},
{Title: "device id", ValuePath: "deviceId"},
{Title: "effects", ValuePath: "effects"},
{Title: "icon", ValuePath: "icon"},
}
Expand All @@ -104,7 +104,7 @@ var getEntitiesCmd = &cobra.Command{
{Title: "name", ValuePath: "name"},
{Title: "object id", ValuePath: "objectId"},
{Title: "key", ValuePath: "key"},
{Title: "unique id", ValuePath: "uniqueId"},
{Title: "device id", ValuePath: "deviceId"},
{Title: "device class", ValuePath: "deviceClass"},
}
printer.Print(cmd.OutOrStdout(), headers, _sensors, rootCmd.HideHeader, rootCmd.OutputFormat, rootCmd.Pretty)
Expand Down
25 changes: 16 additions & 9 deletions cli/command/root/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,17 @@ func GetClient(cfg *cliTY.DeviceConfig, callBackFunc TY.CallBackFunc) (*client.C
if err != nil {
return nil, err
}
if cfg.GetPassword() != "" {
err = _client.Login(cfg.GetPassword())
if err != nil {
return nil, err
}
password := cfg.GetPassword()
if password != "" {
_, _ = fmt.Fprintln(ioStreams.ErrOut,
"WARNING: API password auth is deprecated and was removed in ESPHome 2026.1.0. Prefer --encryption-key / encryptionKey.")
err = _client.Login(password) // legacy password auth (pre-2026.1)
} else {
_, err = _client.Hello()
}
if err != nil {
_ = _client.Close()
return nil, err
}

return _client, nil
Expand All @@ -94,7 +100,7 @@ func GetClient(cfg *cliTY.DeviceConfig, callBackFunc TY.CallBackFunc) (*client.C
func Execute(streams clientTY.IOStreams) {
ioStreams = streams
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(ioStreams.ErrOut, err)
_, _ = fmt.Fprintln(ioStreams.ErrOut, err)
os.Exit(1)
}
}
Expand All @@ -109,11 +115,12 @@ func WriteConfigFile() {

configBytes, err := yaml.Marshal(CONFIG)
if err != nil {
fmt.Fprintf(ioStreams.ErrOut, "error on config file marshal. error:[%s]\n", err.Error())
_, _ = fmt.Fprintf(ioStreams.ErrOut, "error on config file marshal. error:[%s]\n", err.Error())
return
}
err = os.WriteFile(cfgFile, configBytes, os.ModePerm)
if err != nil {
fmt.Fprintf(ioStreams.ErrOut, "error on writing config file to disk, filename:%s, error:[%s]\n", cfgFile, err.Error())
_, _ = fmt.Fprintf(ioStreams.ErrOut, "error on writing config file to disk, filename:%s, error:[%s]\n", cfgFile, err.Error())
}
}

Expand Down Expand Up @@ -142,7 +149,7 @@ func loadConfig() {
if err := viper.ReadInConfig(); err == nil {
err = viper.Unmarshal(&CONFIG)
if err != nil {
fmt.Fprint(ioStreams.ErrOut, "error on unmarshal of config\n", err)
_, _ = fmt.Fprint(ioStreams.ErrOut, "error on unmarshal of config\n", err)
}
}
}
Expand Down
Loading
Loading