Skip to content
This repository was archived by the owner on Jun 20, 2025. It is now read-only.

Commit cae118e

Browse files
authored
Merge pull request #1 from achetronic/feat/add-tapogo-client
feat: Add Tapogo client for KLAP management
2 parents df03f4e + 1f1be94 commit cae118e

File tree

5 files changed

+103
-17
lines changed

5 files changed

+103
-17
lines changed

api/v1alpha1/integrations_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package v1alpha1
22

33
// --
44
type TapoSmartPlugSpec struct {
5+
Client string `yaml:"client"`
56
Address string `yaml:"address"`
67
Auth struct {
78
Username string `yaml:"username" env:"TAPO_SMARTPLUG_USERNAME"`

config/samples/autoheater.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,13 @@ spec:
5959

6060
# Data for sending the events to TAPO P1XX devices (p100, p110, etc)
6161
tapoSmartPlug:
62-
address: "192.168.1.100"
62+
# (Optional) protocol used to communicate with Tapo plugs.
63+
# TPLink tends to change completely the protocol from time to time.
64+
# legacy: is used for older firmware versions and supports 'securePassthrough' protocol
65+
# modern: (default) is used for newer firmware versions and supports 'KLAP' protocol
66+
client: modern
67+
68+
address: "192.168.1.109"
6369

6470
# (Optional) username and password for auth. It's the same used to access the app (it's accessed locally, but
6571
# Tapo devices are configured that way when they are set up)

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/achetronic/autoheater
33
go 1.21
44

55
require (
6+
github.com/achetronic/tapogo v0.1.1
67
github.com/avast/retry-go v3.0.0+incompatible
78
github.com/caarlos0/env/v9 v9.0.0
89
github.com/richardjennings/tapo v0.0.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
github.com/achetronic/tapogo v0.1.1 h1:HbTRMUCl6rnd7Q//R/RSfT9uvJHz2ynFqyb2upbdUAw=
2+
github.com/achetronic/tapogo v0.1.1/go.mod h1:DVflSfbePg4SAjRy0aeMYJ0vj4Pjesw5sw6y4yP0f8w=
13
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
24
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
35
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
Lines changed: 92 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,30 @@
11
package taposmartplug
22

33
import (
4+
"encoding/json"
45
"errors"
56
"github.com/achetronic/autoheater/api/v1alpha1"
7+
tapogotypes "github.com/achetronic/tapogo/api/types"
8+
"github.com/achetronic/tapogo/pkg/tapogo"
69
"github.com/richardjennings/tapo/pkg/tapo"
10+
"time"
711
)
812

913
const (
1014
RequiredConfigFieldsMissingMessage = "some mandatory config field is missing on Tapo SmartPlug integration"
15+
16+
RequestRetryAttempts = 3
1117
)
1218

1319
// --
1420
func checkConfigFields(ctx *v1alpha1.Context) (err error) {
1521
tapoConfig := ctx.Config.Spec.Device.Integrations.TapoSmartPlug
1622

1723
// Check required fields to act
18-
if tapoConfig.Address == "" || tapoConfig.Auth.Username == "" || tapoConfig.Auth.Password == "" {
24+
if tapoConfig.Address == "" ||
25+
tapoConfig.Auth.Username == "" ||
26+
tapoConfig.Auth.Password == "" ||
27+
tapoConfig.Client == "" {
1928
return errors.New(RequiredConfigFieldsMissingMessage)
2029
}
2130

@@ -30,18 +39,51 @@ func TurnOnDevice(ctx *v1alpha1.Context) (tapoResponse map[string]interface{}, e
3039
return tapoResponse, err
3140
}
3241

33-
//
34-
var tapoClient *tapo.Tapo
35-
3642
tapoConfig := ctx.Config.Spec.Device.Integrations.TapoSmartPlug
3743

38-
tapoClient, err = tapo.NewTapo(tapoConfig.Address, tapoConfig.Auth.Username, tapoConfig.Auth.Password)
39-
if err != nil {
40-
return tapoResponse, err
44+
switch tapoConfig.Client {
45+
case "legacy":
46+
tapoClient, err := tapo.NewTapo(tapoConfig.Address, tapoConfig.Auth.Username, tapoConfig.Auth.Password)
47+
if err != nil {
48+
return tapoResponse, err
49+
}
50+
51+
tapoResponse, err = tapoClient.TurnOn()
52+
53+
default:
54+
tapoClientNew, err := tapogo.NewTapo(tapoConfig.Address, tapoConfig.Auth.Username, tapoConfig.Auth.Password)
55+
if err != nil {
56+
return tapoResponse, err
57+
}
58+
59+
// New KLAP protocol throws random errors when the requests are done at speed.
60+
// Retrying mostly solve the issue
61+
tapoResponseNew := &tapogotypes.ResponseSpec{}
62+
for retryAttempt := 0; retryAttempt < RequestRetryAttempts; retryAttempt++ {
63+
tapoResponseNew, err = tapoClientNew.TurnOn()
64+
if err != nil {
65+
time.Sleep(time.Millisecond * 250) // Magic number 0.25s
66+
continue
67+
}
68+
break
69+
}
70+
71+
if err != nil {
72+
return tapoResponse, err
73+
}
74+
75+
// Make response compatible with the global interface
76+
jsonBytes, err := json.Marshal(tapoResponseNew)
77+
if err != nil {
78+
return tapoResponse, err
79+
}
80+
81+
err = json.Unmarshal(jsonBytes, &tapoResponse)
82+
if err != nil {
83+
return tapoResponse, err
84+
}
4185
}
4286

43-
tapoResponse, err = tapoClient.TurnOn()
44-
4587
return tapoResponse, err
4688
}
4789

@@ -54,16 +96,50 @@ func TurnOffDevice(ctx *v1alpha1.Context) (tapoResponse map[string]interface{},
5496
}
5597

5698
//
57-
var tapoClient *tapo.Tapo
58-
5999
tapoConfig := ctx.Config.Spec.Device.Integrations.TapoSmartPlug
60100

61-
tapoClient, err = tapo.NewTapo(tapoConfig.Address, tapoConfig.Auth.Username, tapoConfig.Auth.Password)
62-
if err != nil {
63-
return tapoResponse, err
101+
switch tapoConfig.Client {
102+
case "legacy":
103+
tapoClient, err := tapo.NewTapo(tapoConfig.Address, tapoConfig.Auth.Username, tapoConfig.Auth.Password)
104+
if err != nil {
105+
return tapoResponse, err
106+
}
107+
108+
tapoResponse, err = tapoClient.TurnOff()
109+
110+
default:
111+
tapoClientNew, err := tapogo.NewTapo(tapoConfig.Address, tapoConfig.Auth.Username, tapoConfig.Auth.Password)
112+
if err != nil {
113+
return tapoResponse, err
114+
}
115+
116+
// New KLAP protocol throws random errors when the requests are done at speed.
117+
// Retrying mostly solve the issue
118+
tapoResponseNew := &tapogotypes.ResponseSpec{}
119+
for retryAttempt := 0; retryAttempt < RequestRetryAttempts; retryAttempt++ {
120+
tapoResponseNew, err = tapoClientNew.TurnOff()
121+
if err != nil {
122+
time.Sleep(time.Millisecond * 250) // Magic number 0.25s
123+
continue
124+
}
125+
break
126+
}
127+
128+
if err != nil {
129+
return tapoResponse, err
130+
}
131+
132+
// Make response compatible with the global interface
133+
jsonBytes, err := json.Marshal(tapoResponseNew)
134+
if err != nil {
135+
return tapoResponse, err
136+
}
137+
138+
err = json.Unmarshal(jsonBytes, &tapoResponse)
139+
if err != nil {
140+
return tapoResponse, err
141+
}
64142
}
65143

66-
tapoResponse, err = tapoClient.TurnOff()
67-
68144
return tapoResponse, err
69145
}

0 commit comments

Comments
 (0)