parent
dbf76af86a
commit
ddf56ec6aa
2
go.mod
2
go.mod
|
@ -12,7 +12,6 @@ require (
|
|||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.15.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
)
|
||||
|
@ -28,7 +27,6 @@ require (
|
|||
github.com/nats-io/nats-server/v2 v2.10.2 // indirect
|
||||
github.com/nats-io/nkeys v0.4.5 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/samber/lo v1.38.1
|
||||
golang.org/x/crypto v0.13.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
|
|
8
go.sum
8
go.sum
|
@ -2,6 +2,7 @@ github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
|||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
|
@ -42,19 +43,19 @@ github.com/nats-io/nkeys v0.4.5/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5s
|
|||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||
github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
|
@ -63,8 +64,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
|||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
@ -116,4 +115,5 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
|||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
@ -41,7 +41,7 @@ func Listen(parentLogger *slog.Logger) {
|
|||
router.Post("/api/state/{entityId}/{service}", setEntityStateHandler)
|
||||
|
||||
router.Get("/api/timer/{timerName}", getTimerHandler)
|
||||
router.Post("/api/timer/{timerName}", createTimerHandler)
|
||||
router.Post("/api/timer/{timerName}", startTimerHandler)
|
||||
router.Delete("/api/timer/{timerName}", deleteTimerHandler)
|
||||
|
||||
router.Get("/api/schedule/{scheduleName}", getScheduleHandler)
|
||||
|
@ -100,7 +100,7 @@ func setEntityStateHandler(w http.ResponseWriter, r *http.Request) {
|
|||
entityId := chi.URLParam(r, "entityId")
|
||||
service := chi.URLParam(r, "service")
|
||||
|
||||
var extras map[string]string
|
||||
var extras map[string]any
|
||||
err := render.DecodeJSON(r.Body, &extras)
|
||||
var haErr error
|
||||
if err == nil && len(extras) > 0 {
|
||||
|
@ -131,34 +131,38 @@ func getTimerHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
render.PlainText(w, r, string(timer.Marshall()))
|
||||
render.PlainText(w, r, timer.ToString())
|
||||
}
|
||||
|
||||
type CreateTimerData struct {
|
||||
type StartTimerData struct {
|
||||
Duration string `json:"duration"`
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
|
||||
func createTimerHandler(w http.ResponseWriter, r *http.Request) {
|
||||
func startTimerHandler(w http.ResponseWriter, r *http.Request) {
|
||||
timerName := chi.URLParam(r, "timerName")
|
||||
logRequest(w, r)
|
||||
|
||||
data := &CreateTimerData{}
|
||||
data := &StartTimerData{}
|
||||
if err := render.DecodeJSON(r.Body, data); err != nil {
|
||||
http.Error(w, "Unable to parse timer data", http.StatusNotAcceptable)
|
||||
return
|
||||
}
|
||||
|
||||
if data.Duration == "" {
|
||||
http.Error(w, "duration required", http.StatusNotAcceptable)
|
||||
timer, err := nats.GetTimer(timerName)
|
||||
if err != nil {
|
||||
http.Error(w, "Unable to get timer: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
timer := nats.NewTimerWithDuration(timerName, data.Duration).CalculateNext()
|
||||
if data.Duration == "" {
|
||||
data.Duration = timer.Duration.String()
|
||||
}
|
||||
|
||||
if data.Force {
|
||||
timer.Activate()
|
||||
timer.Activate(data.Duration)
|
||||
} else {
|
||||
timer.ActivateIfNotAlready()
|
||||
timer.ActivateIfNotAlready(data.Duration)
|
||||
}
|
||||
|
||||
getTimerHandler(w, r)
|
||||
|
|
|
@ -22,9 +22,10 @@ var (
|
|||
)
|
||||
|
||||
const (
|
||||
stateChangeEventId = 1001
|
||||
zhaEventId = 1002
|
||||
qrEventId = 1003
|
||||
stateChangeEventId = 1001
|
||||
zhaEventId = 1002
|
||||
qrEventId = 1003
|
||||
timerFinishedEventId = 1005
|
||||
)
|
||||
|
||||
func CloseSubscription() error {
|
||||
|
@ -115,6 +116,10 @@ func handleMessages() {
|
|||
Type: ha.MessageType.SubscribeEvents,
|
||||
EventType: ha.MessageType.TagScanned,
|
||||
Id: qrEventId})
|
||||
haWebsocketConn.WriteJSON(ha.SubscribeEventsMessage{
|
||||
Type: ha.MessageType.SubscribeEvents,
|
||||
EventType: ha.MessageType.TimerFinished,
|
||||
Id: timerFinishedEventId})
|
||||
case ha.MessageType.Result:
|
||||
if !message.Success {
|
||||
logger.Error("Non-Success Result:", "message", message)
|
||||
|
@ -137,6 +142,8 @@ func handleMessages() {
|
|||
case qrEventId:
|
||||
data, _ := json.Marshal(message.Event.Data)
|
||||
nats.Publish(fmt.Sprintf("homeassistant.qr.%s", message.Event.Data.TagId), data)
|
||||
case timerFinishedEventId:
|
||||
nats.PublishString(fmt.Sprintf("homeassistant.%s.finished", message.Event.Data.EntityId), "finished")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,138 +1,94 @@
|
|||
package nats
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
"code.jhot.me/jhot/hats/pkg/config"
|
||||
"code.jhot.me/jhot/hats/pkg/homeassistant"
|
||||
)
|
||||
|
||||
var (
|
||||
timerStore jetstream.KeyValue
|
||||
ticker *time.Ticker
|
||||
haClient *homeassistant.RestClient
|
||||
)
|
||||
|
||||
func TimerStoreConnect() error {
|
||||
if client.JS == nil {
|
||||
return errors.New("jetstream must be connected first")
|
||||
func initHomeAssistantClient() {
|
||||
if haClient == nil {
|
||||
cfg = config.FromEnvironment()
|
||||
haClient = homeassistant.NewRestClient(cfg.GetHomeAssistantBaseUrl(), cfg.HomeAssistantToken)
|
||||
}
|
||||
|
||||
logger.Debug("Looking for KV store")
|
||||
listener := client.JS.KeyValueStoreNames(ctx)
|
||||
found := false
|
||||
for name := range listener.Name() {
|
||||
if name == "KV_hats_timers" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
if found {
|
||||
logger.Debug("Connecting to Timers KV store")
|
||||
timerStore, err = client.JS.KeyValue(ctx, "hats_timers")
|
||||
} else {
|
||||
logger.Debug("Creating Timers KV store")
|
||||
timerStore, err = client.JS.CreateKeyValue(ctx, jetstream.KeyValueConfig{
|
||||
Bucket: "hats_timers",
|
||||
})
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
type HatsTimer struct {
|
||||
Name string
|
||||
Duration time.Duration
|
||||
NextActivation time.Time
|
||||
Name string
|
||||
State string
|
||||
Duration time.Duration
|
||||
FinishTime time.Time
|
||||
}
|
||||
|
||||
func NewTimerWithDuration(name, duration string) *HatsTimer {
|
||||
t := &HatsTimer{
|
||||
Name: name,
|
||||
}
|
||||
|
||||
d, err := time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
d = 5 * time.Minute
|
||||
}
|
||||
t.Duration = d
|
||||
|
||||
return t.CalculateNext()
|
||||
}
|
||||
|
||||
func NewTimerWithActivation(name string, activation []byte) (*HatsTimer, error) {
|
||||
t := &HatsTimer{
|
||||
Name: name,
|
||||
Duration: 5 * time.Minute,
|
||||
}
|
||||
|
||||
a, err := time.Parse(time.RFC3339, string(activation))
|
||||
if err != nil {
|
||||
return t.CalculateNext(), err
|
||||
}
|
||||
t.NextActivation = a
|
||||
|
||||
return t, nil
|
||||
func (t *HatsTimer) getEntityId() string {
|
||||
return fmt.Sprintf("timer.%s", t.Name)
|
||||
}
|
||||
|
||||
func GetTimer(name string) (*HatsTimer, error) {
|
||||
value, err := timerStore.Get(ctx, name)
|
||||
initHomeAssistantClient()
|
||||
state, err := haClient.GetState(fmt.Sprintf("timer.%s", name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewTimerWithActivation(name, value.Value())
|
||||
d, err := homeassistant.ParseDuration(state.Attributes["duration"].(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var finishTime time.Time
|
||||
if finishesAt, ok := state.Attributes["finishes_at"]; ok {
|
||||
finishTime, _ = time.Parse(time.RFC3339, finishesAt.(string))
|
||||
}
|
||||
|
||||
return &HatsTimer{
|
||||
Name: name,
|
||||
State: state.State,
|
||||
Duration: d,
|
||||
FinishTime: finishTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *HatsTimer) CalculateNext() *HatsTimer {
|
||||
t.NextActivation = time.Now().Add(t.Duration)
|
||||
return t
|
||||
func (t *HatsTimer) Activate(durationOverride string) {
|
||||
d := t.Duration
|
||||
if durationOverride != "" {
|
||||
var err error
|
||||
d, err = time.ParseDuration(durationOverride)
|
||||
if err != nil {
|
||||
logger.Error("Error parsing duration", "error", err)
|
||||
d = t.Duration
|
||||
}
|
||||
}
|
||||
logger.Error("Starting timer", "duration", int(d.Seconds()))
|
||||
err := haClient.CallService(t.getEntityId(), homeassistant.Services.Start, map[string]any{
|
||||
homeassistant.ExtraProps.Duration: int(d.Seconds()),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("Error starting timer", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HatsTimer) Marshall() []byte {
|
||||
timestamp, _ := t.NextActivation.MarshalText()
|
||||
return timestamp
|
||||
}
|
||||
|
||||
func (t *HatsTimer) Activate() {
|
||||
timerStore.Put(ctx, t.Name, t.Marshall())
|
||||
}
|
||||
|
||||
func (t *HatsTimer) ActivateIfNotAlready() {
|
||||
timerStore.Create(ctx, t.Name, t.Marshall())
|
||||
func (t *HatsTimer) ActivateIfNotAlready(durationOverride string) {
|
||||
state, err := haClient.GetState(t.getEntityId())
|
||||
if err != nil || state.State != "active" {
|
||||
t.Activate(durationOverride)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *HatsTimer) Cancel() {
|
||||
timerStore.Purge(ctx, t.Name)
|
||||
haClient.CallService(t.getEntityId(), homeassistant.Services.Cancel)
|
||||
}
|
||||
|
||||
func (t *HatsTimer) End() {
|
||||
t.Cancel()
|
||||
PublishString("timers."+t.Name, "done")
|
||||
}
|
||||
|
||||
func WatchTimers() {
|
||||
ticker = time.NewTicker(time.Second)
|
||||
for {
|
||||
t := <-ticker.C
|
||||
timers, _ := timerStore.Keys(ctx, jetstream.IgnoreDeletes())
|
||||
for _, timerName := range timers {
|
||||
timer, err := GetTimer(timerName)
|
||||
if err != nil {
|
||||
logger.Error("Error retrieving timer", "timer", timerName, "error", err)
|
||||
timer.Cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
if t.After(timer.NextActivation) {
|
||||
timer.End()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func StopTimers() {
|
||||
if ticker != nil {
|
||||
ticker.Stop()
|
||||
func (t *HatsTimer) ToString() string {
|
||||
if t.State == "active" {
|
||||
time, _ := t.FinishTime.MarshalText()
|
||||
return string(time)
|
||||
}
|
||||
return t.State
|
||||
}
|
||||
|
|
7
main.go
7
main.go
|
@ -40,13 +40,6 @@ func main() {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
err = nats.TimerStoreConnect()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
go nats.WatchTimers()
|
||||
defer nats.StopTimers()
|
||||
|
||||
err = nats.ScheduleStoreConnect()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
@ -101,13 +101,13 @@ func (c *HatsClient) GetTimer(name string) (string, error) {
|
|||
return resp.String(), nil
|
||||
}
|
||||
|
||||
// Set a timer
|
||||
// Start a timer
|
||||
//
|
||||
// name: the name of the timer (should be unique)
|
||||
// duration: time.Duration string for how long the timer should last
|
||||
// force: if true, will start the timer over even if it is already running
|
||||
func (c *HatsClient) SetTimer(name string, duration string, force bool) (string, error) {
|
||||
data := api.CreateTimerData{
|
||||
func (c *HatsClient) StartTimer(name string, duration string, force bool) (string, error) {
|
||||
data := api.StartTimerData{
|
||||
Duration: duration,
|
||||
Force: force,
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ func (c *HatsClient) SetTimer(name string, duration string, force bool) (string,
|
|||
return resp.String(), nil
|
||||
}
|
||||
|
||||
func (c *HatsClient) DeleteTimer(name string) error {
|
||||
func (c *HatsClient) CancelTimer(name string) error {
|
||||
resp, err := c.client.R().Delete(fmt.Sprintf("api/timer/%s", name))
|
||||
if err == nil && !resp.IsSuccess() {
|
||||
err = fmt.Errorf("%d status code received: %s", resp.StatusCode(), resp.String())
|
||||
|
|
|
@ -31,9 +31,9 @@ func (c *RestClient) GetState(entityId string) (StateData, error) {
|
|||
return data, err
|
||||
}
|
||||
|
||||
func (c *RestClient) CallService(entityId string, service string, extras ...map[string]string) error {
|
||||
func (c *RestClient) CallService(entityId string, service string, extras ...map[string]any) error {
|
||||
domain := strings.Split(entityId, ".")[0]
|
||||
data := map[string]interface{}{
|
||||
data := map[string]any{
|
||||
"entity_id": entityId,
|
||||
}
|
||||
for _, extra := range extras {
|
||||
|
|
|
@ -11,6 +11,8 @@ var MessageType = struct {
|
|||
SubscribeEvents string
|
||||
StateChanged string
|
||||
TagScanned string
|
||||
TimerStarted string
|
||||
TimerFinished string
|
||||
}{
|
||||
AuthRequired: "auth_required",
|
||||
AuthOk: "auth_ok",
|
||||
|
@ -21,6 +23,8 @@ var MessageType = struct {
|
|||
SubscribeEvents: "subscribe_events",
|
||||
StateChanged: "state_changed",
|
||||
TagScanned: "tag_scanned",
|
||||
TimerStarted: "timer.started",
|
||||
TimerFinished: "timer.finished",
|
||||
}
|
||||
|
||||
// Home Assistant device domains
|
||||
|
@ -50,6 +54,9 @@ var Services = struct {
|
|||
SetHvacMode string
|
||||
SetFanMode string
|
||||
SetTemperature string
|
||||
Start string
|
||||
Change string
|
||||
Cancel string
|
||||
}{
|
||||
TurnOn: "turn_on",
|
||||
TurnOff: "turn_off",
|
||||
|
@ -63,6 +70,9 @@ var Services = struct {
|
|||
SetHvacMode: "set_hvac_mode",
|
||||
SetFanMode: "set_fan_mode",
|
||||
SetTemperature: "set_temperature",
|
||||
Start: "start",
|
||||
Change: "change",
|
||||
Cancel: "cancel",
|
||||
}
|
||||
|
||||
// Extra props that can be sent when calling a Home Assistant service
|
||||
|
@ -70,10 +80,20 @@ var ExtraProps = struct {
|
|||
Transition string
|
||||
Brightness string
|
||||
BrightnessPercent string
|
||||
HvacMode string
|
||||
Temperature string
|
||||
TargetTempHigh string
|
||||
TargetTempLow string
|
||||
Duration string
|
||||
}{
|
||||
Transition: "transition",
|
||||
Brightness: "brightness",
|
||||
BrightnessPercent: "brightness_pct",
|
||||
HvacMode: "hvac_mode",
|
||||
Temperature: "temperature",
|
||||
TargetTempHigh: "target_temp_high",
|
||||
TargetTempLow: "target_temp_low",
|
||||
Duration: "duration",
|
||||
}
|
||||
|
||||
type ResultContext struct {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package homeassistant
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StateToBool converts a state string into a boolean
|
||||
|
@ -43,3 +45,21 @@ func BoolToService(entityId string, desiredState bool) string {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a Home Assistant duration in HH:MM:SS format
|
||||
func ParseDuration(d string) (time.Duration, error) {
|
||||
var hour, min, sec int
|
||||
_, err := fmt.Sscanf(d, "%d:%d:%d", &hour, &min, &sec)
|
||||
if err != nil {
|
||||
return 5 * time.Minute, err
|
||||
}
|
||||
|
||||
return time.Duration(hour)*time.Hour + time.Duration(min)*time.Minute + time.Duration(sec)*time.Second, nil
|
||||
}
|
||||
|
||||
// func FormatDuration(d time.Duration) string {
|
||||
// hours := math.Floor(d.Hours())
|
||||
// minutes := math.Floor(d.Minutes() - (hours * 60))
|
||||
// seconds := math.Floor(d.Seconds() - (hours * 60 * 60) - (minutes * 60))
|
||||
// return fmt.Sprintf("%02.f:%02.f:%02.f", hours, minutes, seconds)
|
||||
// }
|
||||
|
|
Loading…
Reference in New Issue