20 Commits

Author SHA1 Message Date
e13fd6ab5b chore: update carbon pkg to v2 latest, bump version 2025-08-01 21:02:21 -05:00
d281d70d8c chore: update dependencies 2025-08-01 20:57:25 -05:00
8fe6bc0cff chore: reformat, normalize imports 2025-08-01 20:44:38 -05:00
9b8ef545a6 refactor: websockets into 'connect' module, rename & adjust generally 2025-08-01 20:34:45 -05:00
102a4e7438 refactor: minor changes 2025-08-01 18:25:39 -05:00
d51f6d5946 refactor: move 'parse' back to internal root 2025-08-01 18:18:16 -05:00
26b8892ff6 refactor: move event_types into types/ pkg, make methods public, small fixes 2025-08-01 18:16:25 -05:00
21358b73e1 refactor: move types out of app.go into types/, renamed module files 2025-08-01 18:08:01 -05:00
3d178ad05e refactor: rename request_types.go 2025-08-01 17:10:03 -05:00
c91c4f85c3 refactor: rename module to github.com/Xevion/go-ha 2025-08-01 17:04:11 -05:00
5698a30b37 refactor: move http module into internal root, remove deprecated handling 2025-08-01 17:03:28 -05:00
a8d4cefaab chore: remove extension from LICENSE file
Also slight refactor for NewBaseServiceRequest
2025-08-01 16:55:44 -05:00
7081d06001 refactor: rename files into common naming, rename GetId to NextId, document function 2025-08-01 16:55:44 -05:00
55a390e69c refactor: move parsing into internal/parse module, move other functions into misc 2025-08-01 16:55:44 -05:00
378bc29e7e refactor: use atomic.Int64 for underlying GetId implementation 2025-08-01 16:55:44 -05:00
393191ccb5 refactor: remove unused http.go post() method 2025-08-01 16:55:44 -05:00
e35f296d70 refactor: remove internal priority_queue module, switch to Workiva implementation 2025-08-01 16:55:44 -05:00
b3001e7f6e fix: fix always cancelled connection context, improper ctx 2025-08-01 16:55:44 -05:00
2bcb4c2594 feat: better close message, add channel capacity 2025-08-01 16:55:44 -05:00
a71ec6ed25 chore: tidy module 2025-08-01 16:55:44 -05:00
55 changed files with 855 additions and 957 deletions

View File

View File

@@ -1,20 +1,18 @@
# Gome-Assistant # go-ha
Write strongly typed [Home Assistant](https://www.home-assistant.io/) automations in Go! Write strongly typed [Home Assistant](https://www.home-assistant.io/) automations in Go!
## Disclaimer ```bash
go get github.com/Xevion/go-ha
Gome-Assistant is a new library, and I'm opening it up early to get some user feedback on the API and help shape the direction. I plan for it to grow to cover all Home Assistant use cases, services, and event types. So it's possible that breaking changes will happen before v1.0.0!
## Quick Start
### Installation
```
go get github.com/Xevion/gome-assistant
``` ```
### Generate Entity Constants or in `go.mod`:
```go
require github.com/Xevion/go-ha
```
## Generate Entity Constants
You can generate type-safe constants for all your Home Assistant entities using `go generate`. This makes it easier to reference entities in your code. You can generate type-safe constants for all your Home Assistant entities using `go generate`. This makes it easier to reference entities in your code.
@@ -37,7 +35,7 @@ exclude_domains: ["device_tracker", "person"]
2. Add a `//go:generate` comment in your project: 2. Add a `//go:generate` comment in your project:
```go ```go
//go:generate go run github.com/Xevion/gome-assistant/cmd/generate //go:generate go run github.com/Xevion/go-ha/cmd/generate
``` ```
Optionally use the `-config` flag to customize the file path of the config file. Optionally use the `-config` flag to customize the file path of the config file.
@@ -72,11 +70,11 @@ Check out [`example/example.go`](./example/example.go) for an example of the 3 t
### Run your code ### Run your code
Keeping with the simplicity that Go is famous for, you don't need a specific environment or docker container to run Gome-Assistant. You just write and run your code like any other Go binary. So once you build your code, you can run it however you like — using `screen` or `tmux`, a cron job, a linux service, or wrap it up in a docker container if you like! Keeping with the simplicity that Go is famous for, you don't need a specific environment or docker container to run go-ha. You just write and run your code like any other Go binary. So once you build your code, you can run it however you like — using `screen` or `tmux`, a cron job, a linux service, or wrap it up in a docker container if you like!
> _❗ No promises, but I may provide a Docker image with file watching to automatically restart gome-assistant, to make it easier to use gome-assistant on a fully managed Home Assistant installation._ > _❗ No promises, but I may provide a Docker image with file watching to automatically restart go-ha, to make it easier to use go-ha on a fully managed Home Assistant installation._
## gome-assistant Concepts ## go-ha Concepts
### Overview ### Overview
@@ -87,13 +85,13 @@ The general flow is
3. Start app 3. Start app
```go ```go
import ga "github.com/Xevion/gome-assistant" import ha "github.com/Xevion/go-ha"
// replace with IP and port of your Home Assistant installation // replace with IP and port of your Home Assistant installation
app, err := ga.NewApp(ga.NewAppRequest{ app, err := ga.NewApp(ga.NewAppRequest{
URL: "http://192.168.1.123:8123", URL: "http://192.168.1.123:8123",
HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), HAAuthToken: os.Getenv("HA_AUTH_TOKEN"),
HomeZoneEntityId: "zone.home", HomeZoneEntityId: "zone.home",
}) })
// create automations here (see next sections) // create automations here (see next sections)
@@ -107,7 +105,7 @@ app.RegisterIntervals(...)
app.Start() app.Start()
``` ```
A full reference is available on [pkg.go.dev](https://pkg.go.dev/github.com/Xevion/gome-assistant), but all you need to know to get started are the four types of automations in gome-assistant. A full reference is available on [pkg.go.dev](https://pkg.go.dev/github.com/Xevion/go-ha), but all you need to know to get started are the four types of automations in go-ha.
- [Daily Schedules](#daily-schedule) - [Daily Schedules](#daily-schedule)
- [Entity Listeners](#entity-listener) - [Entity Listeners](#entity-listener)
@@ -226,7 +224,7 @@ func myCallback(service *ga.Service, state ga.State, data ga.EventData) {
} }
``` ```
> 💡 Check `eventTypes.go` for pre-defined event types, or create your own struct type for custom events and contribute them back to gome-assistant with a PR. > 💡 Check `eventTypes.go` for pre-defined event types, or create your own struct type for custom events and contribute them back to go-ha with a PR.
### Interval ### Interval

232
app.go
View File

@@ -9,14 +9,14 @@ import (
"strings" "strings"
"time" "time"
"github.com/golang-module/carbon" "github.com/dromara/carbon/v2"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
sunriseLib "github.com/nathan-osman/go-sunrise" sunriseLib "github.com/nathan-osman/go-sunrise"
"github.com/Xevion/gome-assistant/internal" "github.com/Workiva/go-datastructures/queue"
"github.com/Xevion/gome-assistant/internal/http" "github.com/Xevion/go-ha/internal"
pq "github.com/Xevion/gome-assistant/internal/priority_queue" "github.com/Xevion/go-ha/internal/connect"
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/types"
) )
var ErrInvalidArgs = errors.New("invalid arguments provided") var ErrInvalidArgs = errors.New("invalid arguments provided")
@@ -24,66 +24,31 @@ var ErrInvalidArgs = errors.New("invalid arguments provided")
type App struct { type App struct {
ctx context.Context ctx context.Context
ctxCancel context.CancelFunc ctxCancel context.CancelFunc
conn *websocket.Conn
// Wraps the ws connection with added mutex locking // Wraps the ws connection with added mutex locking
wsWriter *ws.WebsocketWriter conn *connect.HAConnection
httpClient *http.HttpClient httpClient *internal.HttpClient
service *Service service *Service
state *StateImpl state *StateImpl
schedules pq.PriorityQueue schedules *queue.PriorityQueue
intervals pq.PriorityQueue intervals *queue.PriorityQueue
entityListeners map[string][]*EntityListener entityListeners map[string][]*EntityListener
entityListenersId int64 entityListenersId int64
eventListeners map[string][]*EventListener eventListeners map[string][]*EventListener
} }
// DurationString represents a duration, such as "2s" or "24h". type Item types.Item
// See https://pkg.go.dev/time#ParseDuration for all valid time units.
type DurationString string
// TimeString is a 24-hr format time "HH:MM" such as "07:30". func (mi Item) Compare(other queue.Item) int {
type TimeString string if mi.Priority > other.(Item).Priority {
return 1
type timeRange struct { } else if mi.Priority == other.(Item).Priority {
start time.Time return 0
end time.Time }
} return -1
type NewAppRequest struct {
// Required
URL string
// Optional
// Deprecated: use URL instead
// IpAddress of your Home Assistant instance i.e. "localhost"
// or "192.168.86.59" etc.
IpAddress string
// Optional
// Deprecated: use URL instead
// Port number Home Assistant is running on. Defaults to 8123.
Port string
// Required
// Auth token generated in Home Assistant. Used
// to connect to the Websocket API.
HAAuthToken string
// Required
// EntityId of the zone representing your home e.g. "zone.home".
// Used to pull latitude/longitude from Home Assistant
// to calculate sunset/sunrise times.
HomeZoneEntityId string
// Optional
// Whether to use secure connections for http and websockets.
// Setting this to `true` will use `https://` instead of `https://`
// and `wss://` instead of `ws://`.
Secure bool
} }
// validateHomeZone verifies that the home zone entity exists and has latitude/longitude // validateHomeZone verifies that the home zone entity exists and has latitude/longitude
@@ -101,11 +66,9 @@ func validateHomeZone(state State, entityID string) error {
// Verify it has latitude and longitude // Verify it has latitude and longitude
if entity.Attributes == nil { if entity.Attributes == nil {
return fmt.Errorf("home zone entity '%s' has no attributes", entityID) return fmt.Errorf("home zone entity '%s' has no attributes", entityID)
} } else if entity.Attributes["latitude"] == nil {
if entity.Attributes["latitude"] == nil {
return fmt.Errorf("home zone entity '%s' missing latitude attribute", entityID) return fmt.Errorf("home zone entity '%s' missing latitude attribute", entityID)
} } else if entity.Attributes["longitude"] == nil {
if entity.Attributes["longitude"] == nil {
return fmt.Errorf("home zone entity '%s' missing longitude attribute", entityID) return fmt.Errorf("home zone entity '%s' missing longitude attribute", entityID)
} }
@@ -116,7 +79,7 @@ func validateHomeZone(state State, entityID string) error {
NewApp establishes the websocket connection and returns an object NewApp establishes the websocket connection and returns an object
you can use to register schedules and listeners. you can use to register schedules and listeners.
*/ */
func NewApp(request NewAppRequest) (*App, error) { func NewApp(request types.NewAppRequest) (*App, error) {
if (request.URL == "" && request.IpAddress == "") || request.HAAuthToken == "" { if (request.URL == "" && request.IpAddress == "") || request.HAAuthToken == "" {
slog.Error("URL and HAAuthToken are required arguments in NewAppRequest") slog.Error("URL and HAAuthToken are required arguments in NewAppRequest")
return nil, ErrInvalidArgs return nil, ErrInvalidArgs
@@ -133,33 +96,18 @@ func NewApp(request NewAppRequest) (*App, error) {
var err error var err error
baseURL, err = url.Parse(request.URL) baseURL, err = url.Parse(request.URL)
if err != nil { if err != nil {
return nil, ErrInvalidArgs return nil, fmt.Errorf("failed to parse URL: %w", err)
} }
} else {
// This is deprecated and will be removed in a future release
port := request.Port
if port == "" {
port = "8123"
}
baseURL.Scheme = "http"
if request.Secure {
baseURL.Scheme = "https"
}
baseURL.Host = request.IpAddress + ":" + port
} }
conn, ctx, ctxCancel, err := ws.ConnectionFromUri(baseURL, request.HAAuthToken) conn, ctx, ctxCancel, err := connect.ConnectionFromUri(baseURL, request.HAAuthToken)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if conn == nil {
return nil, err
}
httpClient := http.NewHttpClient(baseURL, request.HAAuthToken) httpClient := internal.NewHttpClient(ctx, baseURL, request.HAAuthToken)
wsWriter := &ws.WebsocketWriter{Conn: conn} service := newService(conn)
service := newService(wsWriter)
state, err := newState(httpClient, request.HomeZoneEntityId) state, err := newState(httpClient, request.HomeZoneEntityId)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -172,49 +120,53 @@ func NewApp(request NewAppRequest) (*App, error) {
return &App{ return &App{
conn: conn, conn: conn,
wsWriter: wsWriter,
ctx: ctx, ctx: ctx,
ctxCancel: ctxCancel, ctxCancel: ctxCancel,
httpClient: httpClient, httpClient: httpClient,
service: service, service: service,
state: state, state: state,
schedules: pq.New(), schedules: queue.NewPriorityQueue(100, false),
intervals: pq.New(), intervals: queue.NewPriorityQueue(100, false),
entityListeners: map[string][]*EntityListener{}, entityListeners: map[string][]*EntityListener{},
eventListeners: map[string][]*EventListener{}, eventListeners: map[string][]*EventListener{},
}, nil }, nil
} }
func (a *App) Cleanup() { func (app *App) Cleanup() {
if a.ctxCancel != nil { if app.ctxCancel != nil {
a.ctxCancel() app.ctxCancel()
} }
} }
// Close performs a clean shutdown of the application. // Close performs a clean shutdown of the application.
// It cancels the context, closes the websocket connection, // It cancels the context, closes the websocket connection,
// and ensures all background processes are properly terminated. // and ensures all background processes are properly terminated.
func (a *App) Close() error { func (app *App) Close() error {
// Cancel context to signal all goroutines to stop
if a.ctxCancel != nil {
a.ctxCancel()
}
// Close websocket connection if it exists // Close websocket connection if it exists
if a.conn != nil { if app.conn != nil {
// Send close message to Home Assistant deadline := time.Now().Add(10 * time.Second)
closeMsg := map[string]string{ err := app.conn.Conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), deadline)
"type": "close", if err != nil {
slog.Warn("Error writing close message", "error", err)
return err
} }
_ = a.conn.WriteJSON(closeMsg)
// Close the websocket connection // Close the websocket connection
err := a.conn.Close() err = app.conn.Conn.Close()
if err != nil { if err != nil {
slog.Warn("Error closing websocket connection", "error", err) slog.Warn("Error closing websocket connection", "error", err)
return err
} }
} }
// Wait a short time for the websocket connection to close
time.Sleep(500 * time.Millisecond)
// Cancel context to signal all goroutines to stop
if app.ctxCancel != nil {
app.ctxCancel()
}
// Wait a short time for goroutines to finish // Wait a short time for goroutines to finish
// This allows for graceful shutdown of background processes // This allows for graceful shutdown of background processes
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@@ -222,12 +174,12 @@ func (a *App) Close() error {
return nil return nil
} }
func (a *App) RegisterSchedules(schedules ...DailySchedule) { func (app *App) RegisterSchedules(schedules ...DailySchedule) {
for _, s := range schedules { for _, s := range schedules {
// realStartTime already set for sunset/sunrise // realStartTime already set for sunset/sunrise
if s.isSunrise || s.isSunset { if s.isSunrise || s.isSunset {
s.nextRunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset).Carbon2Time() s.nextRunTime = getNextSunRiseOrSet(app, s.isSunrise, s.sunOffset).StdTime()
a.schedules.Insert(s, float64(s.nextRunTime.Unix())) app.schedules.Put()
continue continue
} }
@@ -239,28 +191,34 @@ func (a *App) RegisterSchedules(schedules ...DailySchedule) {
startTime = startTime.AddDay() startTime = startTime.AddDay()
} }
s.nextRunTime = startTime.Carbon2Time() s.nextRunTime = startTime.StdTime()
a.schedules.Insert(s, float64(startTime.Carbon2Time().Unix())) app.schedules.Put(Item{
Value: s,
Priority: float64(startTime.StdTime().Unix()),
})
} }
} }
func (a *App) RegisterIntervals(intervals ...Interval) { func (app *App) RegisterIntervals(intervals ...Interval) {
for _, i := range intervals { for _, i := range intervals {
if i.frequency == 0 { if i.frequency == 0 {
slog.Error("A schedule must use either set frequency via Every()") slog.Error("A schedule must use either set frequency via Every()")
panic(ErrInvalidArgs) panic(ErrInvalidArgs)
} }
i.nextRunTime = internal.ParseTime(string(i.startTime)).Carbon2Time() i.nextRunTime = internal.ParseTime(string(i.startTime)).StdTime()
now := time.Now() now := time.Now()
for i.nextRunTime.Before(now) { for i.nextRunTime.Before(now) {
i.nextRunTime = i.nextRunTime.Add(i.frequency) i.nextRunTime = i.nextRunTime.Add(i.frequency)
} }
a.intervals.Insert(i, float64(i.nextRunTime.Unix())) app.intervals.Put(Item{
Value: i,
Priority: float64(i.nextRunTime.Unix()),
})
} }
} }
func (a *App) RegisterEntityListeners(etls ...EntityListener) { func (app *App) RegisterEntityListeners(etls ...EntityListener) {
for _, etl := range etls { for _, etl := range etls {
etl := etl etl := etl
if etl.delay != 0 && etl.toState == "" { if etl.delay != 0 && etl.toState == "" {
@@ -269,31 +227,31 @@ func (a *App) RegisterEntityListeners(etls ...EntityListener) {
} }
for _, entity := range etl.entityIds { for _, entity := range etl.entityIds {
if elList, ok := a.entityListeners[entity]; ok { if elList, ok := app.entityListeners[entity]; ok {
a.entityListeners[entity] = append(elList, &etl) app.entityListeners[entity] = append(elList, &etl)
} else { } else {
a.entityListeners[entity] = []*EntityListener{&etl} app.entityListeners[entity] = []*EntityListener{&etl}
} }
} }
} }
} }
func (a *App) RegisterEventListeners(evls ...EventListener) { func (app *App) RegisterEventListeners(evls ...EventListener) {
for _, evl := range evls { for _, evl := range evls {
evl := evl evl := evl
for _, eventType := range evl.eventTypes { for _, eventType := range evl.eventTypes {
if elList, ok := a.eventListeners[eventType]; ok { if elList, ok := app.eventListeners[eventType]; ok {
a.eventListeners[eventType] = append(elList, &evl) app.eventListeners[eventType] = append(elList, &evl)
} else { } else {
ws.SubscribeToEventType(eventType, a.wsWriter, a.ctx) connect.SubscribeToEventType(eventType, app.conn, app.ctx)
a.eventListeners[eventType] = []*EventListener{&evl} app.eventListeners[eventType] = []*EventListener{&evl}
} }
} }
} }
} }
func getSunriseSunset(s *StateImpl, sunrise bool, dateToUse carbon.Carbon, offset ...DurationString) carbon.Carbon { func getSunriseSunset(s *StateImpl, sunrise bool, dateToUse *carbon.Carbon, offset ...types.DurationString) *carbon.Carbon {
date := dateToUse.Carbon2Time() date := dateToUse.StdTime()
rise, set := sunriseLib.SunriseSunset(s.latitude, s.longitude, date.Year(), date.Month(), date.Day()) rise, set := sunriseLib.SunriseSunset(s.latitude, s.longitude, date.Year(), date.Month(), date.Day())
rise, set = rise.Local(), set.Local() rise, set = rise.Local(), set.Local()
@@ -325,7 +283,7 @@ func getSunriseSunset(s *StateImpl, sunrise bool, dateToUse carbon.Carbon, offse
return setOrRiseToday return setOrRiseToday
} }
func getNextSunRiseOrSet(a *App, sunrise bool, offset ...DurationString) carbon.Carbon { func getNextSunRiseOrSet(a *App, sunrise bool, offset ...types.DurationString) *carbon.Carbon {
sunriseOrSunset := getSunriseSunset(a.state, sunrise, carbon.Now(), offset...) sunriseOrSunset := getSunriseSunset(a.state, sunrise, carbon.Now(), offset...)
if sunriseOrSunset.Lt(carbon.Now()) { if sunriseOrSunset.Lt(carbon.Now()) {
// if we're past today's sunset or sunrise (accounting for offset) then get tomorrows // if we're past today's sunset or sunrise (accounting for offset) then get tomorrows
@@ -335,32 +293,32 @@ func getNextSunRiseOrSet(a *App, sunrise bool, offset ...DurationString) carbon.
return sunriseOrSunset return sunriseOrSunset
} }
func (a *App) Start() { func (app *App) Start() {
slog.Info("Starting", "schedules", a.schedules.Len()) slog.Info("Starting", "schedules", app.schedules.Len())
slog.Info("Starting", "entity listeners", len(a.entityListeners)) slog.Info("Starting", "entity listeners", len(app.entityListeners))
slog.Info("Starting", "event listeners", len(a.eventListeners)) slog.Info("Starting", "event listeners", len(app.eventListeners))
go runSchedules(a) go runSchedules(app)
go runIntervals(a) go runIntervals(app)
// subscribe to state_changed events // subscribe to state_changed events
id := internal.GetId() id := internal.NextId()
ws.SubscribeToStateChangedEvents(id, a.wsWriter, a.ctx) connect.SubscribeToStateChangedEvents(id, app.conn, app.ctx)
a.entityListenersId = id app.entityListenersId = id
// entity listeners runOnStartup // Run entity listeners startup
for eid, etls := range a.entityListeners { for eid, etls := range app.entityListeners {
for _, etl := range etls { for _, etl := range etls {
// ensure each ETL only runs once, even if // ensure each ETL only runs once, even if
// it listens to multiple entities // it listens to multiple entities
if etl.runOnStartup && !etl.runOnStartupCompleted { if etl.runOnStartup && !etl.runOnStartupCompleted {
entityState, err := a.state.Get(eid) entityState, err := app.state.Get(eid)
if err != nil { if err != nil {
slog.Warn("Failed to get entity state \"", eid, "\" during startup, skipping RunOnStartup") slog.Warn("Failed to get entity state \"", eid, "\" during startup, skipping RunOnStartup")
} }
etl.runOnStartupCompleted = true etl.runOnStartupCompleted = true
go etl.callback(a.service, a.state, EntityData{ go etl.callback(app.service, app.state, EntityData{
TriggerEntityId: eid, TriggerEntityId: eid,
FromState: entityState.State, FromState: entityState.State,
FromAttributes: entityState.Attributes, FromAttributes: entityState.Attributes,
@@ -373,8 +331,8 @@ func (a *App) Start() {
} }
// entity listeners and event listeners // entity listeners and event listeners
elChan := make(chan ws.ChanMsg) elChan := make(chan connect.ChannelMessage, 100) // Add buffer to prevent channel overflow
go ws.ListenWebsocket(a.conn, elChan) go connect.ListenWebsocket(app.conn.Conn, elChan)
for { for {
select { select {
@@ -383,22 +341,22 @@ func (a *App) Start() {
slog.Info("Websocket channel closed, stopping main loop") slog.Info("Websocket channel closed, stopping main loop")
return return
} }
if a.entityListenersId == msg.Id { if app.entityListenersId == msg.Id {
go callEntityListeners(a, msg.Raw) go callEntityListeners(app, msg.Raw)
} else { } else {
go callEventListeners(a, msg) go callEventListeners(app, msg)
} }
case <-a.ctx.Done(): case <-app.ctx.Done():
slog.Info("Context cancelled, stopping main loop") slog.Info("Context cancelled, stopping main loop")
return return
} }
} }
} }
func (a *App) GetService() *Service { func (app *App) GetService() *Service {
return a.service return app.service
} }
func (a *App) GetState() State { func (app *App) GetState() State {
return a.state return app.state
} }

View File

@@ -3,16 +3,17 @@ package gomeassistant
import ( import (
"time" "time"
"github.com/Xevion/gome-assistant/internal" "github.com/Xevion/go-ha/internal"
"github.com/golang-module/carbon" "github.com/Xevion/go-ha/types"
"github.com/dromara/carbon/v2"
) )
type conditionCheck struct { type ConditionCheck struct {
fail bool fail bool
} }
func checkWithinTimeRange(startTime, endTime string) conditionCheck { func CheckWithinTimeRange(startTime, endTime string) ConditionCheck {
cc := conditionCheck{fail: false} cc := ConditionCheck{fail: false}
// if betweenStart and betweenEnd both set, first account for midnight // if betweenStart and betweenEnd both set, first account for midnight
// overlap, then check if between those times. // overlap, then check if between those times.
if startTime != "" && endTime != "" { if startTime != "" && endTime != "" {
@@ -42,8 +43,8 @@ func checkWithinTimeRange(startTime, endTime string) conditionCheck {
return cc return cc
} }
func checkStatesMatch(listenerState, s string) conditionCheck { func CheckStatesMatch(listenerState, s string) ConditionCheck {
cc := conditionCheck{fail: false} cc := ConditionCheck{fail: false}
// check if fromState or toState are set and don't match // check if fromState or toState are set and don't match
if listenerState != "" && listenerState != s { if listenerState != "" && listenerState != s {
cc.fail = true cc.fail = true
@@ -51,8 +52,8 @@ func checkStatesMatch(listenerState, s string) conditionCheck {
return cc return cc
} }
func checkThrottle(throttle time.Duration, lastRan carbon.Carbon) conditionCheck { func CheckThrottle(throttle time.Duration, lastRan *carbon.Carbon) ConditionCheck {
cc := conditionCheck{fail: false} cc := ConditionCheck{fail: false}
// check if Throttle is set and that duration hasn't passed since lastRan // check if Throttle is set and that duration hasn't passed since lastRan
if throttle.Seconds() > 0 && if throttle.Seconds() > 0 &&
lastRan.DiffAbsInSeconds(carbon.Now()) < int64(throttle.Seconds()) { lastRan.DiffAbsInSeconds(carbon.Now()) < int64(throttle.Seconds()) {
@@ -61,8 +62,8 @@ func checkThrottle(throttle time.Duration, lastRan carbon.Carbon) conditionCheck
return cc return cc
} }
func checkExceptionDates(eList []time.Time) conditionCheck { func CheckExceptionDates(eList []time.Time) ConditionCheck {
cc := conditionCheck{fail: false} cc := ConditionCheck{fail: false}
for _, e := range eList { for _, e := range eList {
y1, m1, d1 := e.Date() y1, m1, d1 := e.Date()
y2, m2, d2 := time.Now().Date() y2, m2, d2 := time.Now().Date()
@@ -74,11 +75,11 @@ func checkExceptionDates(eList []time.Time) conditionCheck {
return cc return cc
} }
func checkExceptionRanges(eList []timeRange) conditionCheck { func CheckExceptionRanges(eList []types.TimeRange) ConditionCheck {
cc := conditionCheck{fail: false} cc := ConditionCheck{fail: false}
now := time.Now() now := time.Now()
for _, eRange := range eList { for _, eRange := range eList {
if now.After(eRange.start) && now.Before(eRange.end) { if now.After(eRange.Start) && now.Before(eRange.End) {
cc.fail = true cc.fail = true
break break
} }
@@ -86,8 +87,8 @@ func checkExceptionRanges(eList []timeRange) conditionCheck {
return cc return cc
} }
func checkEnabledEntity(s State, infos []internal.EnabledDisabledInfo) conditionCheck { func CheckEnabledEntity(s State, infos []internal.EnabledDisabledInfo) ConditionCheck {
cc := conditionCheck{fail: false} cc := ConditionCheck{fail: false}
if len(infos) == 0 { if len(infos) == 0 {
return cc return cc
} }
@@ -114,8 +115,8 @@ func checkEnabledEntity(s State, infos []internal.EnabledDisabledInfo) condition
return cc return cc
} }
func checkDisabledEntity(s State, infos []internal.EnabledDisabledInfo) conditionCheck { func CheckDisabledEntity(s State, infos []internal.EnabledDisabledInfo) ConditionCheck {
cc := conditionCheck{fail: false} cc := ConditionCheck{fail: false}
if len(infos) == 0 { if len(infos) == 0 {
return cc return cc
} }
@@ -143,12 +144,12 @@ func checkDisabledEntity(s State, infos []internal.EnabledDisabledInfo) conditio
return cc return cc
} }
func checkAllowlistDates(eList []time.Time) conditionCheck { func CheckAllowlistDates(eList []time.Time) ConditionCheck {
if len(eList) == 0 { if len(eList) == 0 {
return conditionCheck{fail: false} return ConditionCheck{fail: false}
} }
cc := conditionCheck{fail: true} cc := ConditionCheck{fail: true}
for _, e := range eList { for _, e := range eList {
y1, m1, d1 := e.Date() y1, m1, d1 := e.Date()
y2, m2, d2 := time.Now().Date() y2, m2, d2 := time.Now().Date()
@@ -160,15 +161,15 @@ func checkAllowlistDates(eList []time.Time) conditionCheck {
return cc return cc
} }
func checkStartEndTime(s TimeString, isStart bool) conditionCheck { func CheckStartEndTime(s types.TimeString, isStart bool) ConditionCheck {
cc := conditionCheck{fail: false} cc := ConditionCheck{fail: false}
// pass immediately if default // pass immediately if default
if s == "00:00" { if s == "00:00" {
return cc return cc
} }
now := time.Now() now := time.Now()
parsedTime := internal.ParseTime(string(s)).Carbon2Time() parsedTime := internal.ParseTime(string(s)).StdTime()
if isStart { if isStart {
if parsedTime.After(now) { if parsedTime.After(now) {
cc.fail = true cc.fail = true

View File

@@ -4,7 +4,8 @@ import (
"errors" "errors"
"testing" "testing"
"github.com/Xevion/gome-assistant/internal" "github.com/Xevion/go-ha/internal"
"github.com/Xevion/go-ha/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -15,16 +16,16 @@ type MockState struct {
GetError bool GetError bool
} }
func (s MockState) AfterSunrise(_ ...DurationString) bool { func (s MockState) AfterSunrise(_ ...types.DurationString) bool {
return true return true
} }
func (s MockState) BeforeSunrise(_ ...DurationString) bool { func (s MockState) BeforeSunrise(_ ...types.DurationString) bool {
return true return true
} }
func (s MockState) AfterSunset(_ ...DurationString) bool { func (s MockState) AfterSunset(_ ...types.DurationString) bool {
return true return true
} }
func (s MockState) BeforeSunset(_ ...DurationString) bool { func (s MockState) BeforeSunset(_ ...types.DurationString) bool {
return true return true
} }
func (s MockState) Get(eid string) (EntityState, error) { func (s MockState) Get(eid string) (EntityState, error) {
@@ -65,7 +66,7 @@ func TestEnabledEntity_StateEqual_Passes(t *testing.T) {
state := MockState{ state := MockState{
EqualsReturn: true, EqualsReturn: true,
} }
c := checkEnabledEntity(state, list(runOnError)) c := CheckEnabledEntity(state, list(runOnError))
assert.False(t, c.fail, "should pass") assert.False(t, c.fail, "should pass")
} }
@@ -73,7 +74,7 @@ func TestEnabledEntity_StateNotEqual_Fails(t *testing.T) {
state := MockState{ state := MockState{
EqualsReturn: false, EqualsReturn: false,
} }
c := checkEnabledEntity(state, list(runOnError)) c := CheckEnabledEntity(state, list(runOnError))
assert.True(t, c.fail, "should fail") assert.True(t, c.fail, "should fail")
} }
@@ -81,7 +82,7 @@ func TestEnabledEntity_NetworkError_DontRun_Fails(t *testing.T) {
state := MockState{ state := MockState{
EqualsError: true, EqualsError: true,
} }
c := checkEnabledEntity(state, list(dontRunOnError)) c := CheckEnabledEntity(state, list(dontRunOnError))
assert.True(t, c.fail, "should fail") assert.True(t, c.fail, "should fail")
} }
@@ -89,7 +90,7 @@ func TestEnabledEntity_NetworkError_StillRun_Passes(t *testing.T) {
state := MockState{ state := MockState{
EqualsError: true, EqualsError: true,
} }
c := checkEnabledEntity(state, list(runOnError)) c := CheckEnabledEntity(state, list(runOnError))
assert.False(t, c.fail, "should fail") assert.False(t, c.fail, "should fail")
} }
@@ -97,7 +98,7 @@ func TestDisabledEntity_StateEqual_Fails(t *testing.T) {
state := MockState{ state := MockState{
EqualsReturn: true, EqualsReturn: true,
} }
c := checkDisabledEntity(state, list(runOnError)) c := CheckDisabledEntity(state, list(runOnError))
assert.True(t, c.fail, "should pass") assert.True(t, c.fail, "should pass")
} }
@@ -105,7 +106,7 @@ func TestDisabledEntity_StateNotEqual_Passes(t *testing.T) {
state := MockState{ state := MockState{
EqualsReturn: false, EqualsReturn: false,
} }
c := checkDisabledEntity(state, list(runOnError)) c := CheckDisabledEntity(state, list(runOnError))
assert.False(t, c.fail, "should fail") assert.False(t, c.fail, "should fail")
} }
@@ -113,7 +114,7 @@ func TestDisabledEntity_NetworkError_DontRun_Fails(t *testing.T) {
state := MockState{ state := MockState{
EqualsError: true, EqualsError: true,
} }
c := checkDisabledEntity(state, list(dontRunOnError)) c := CheckDisabledEntity(state, list(dontRunOnError))
assert.True(t, c.fail, "should fail") assert.True(t, c.fail, "should fail")
} }
@@ -121,16 +122,16 @@ func TestDisabledEntity_NetworkError_StillRun_Passes(t *testing.T) {
state := MockState{ state := MockState{
EqualsError: true, EqualsError: true,
} }
c := checkDisabledEntity(state, list(runOnError)) c := CheckDisabledEntity(state, list(runOnError))
assert.False(t, c.fail, "should fail") assert.False(t, c.fail, "should fail")
} }
func TestStatesMatch(t *testing.T) { func TestStatesMatch(t *testing.T) {
c := checkStatesMatch("hey", "hey") c := CheckStatesMatch("hey", "hey")
assert.False(t, c.fail, "should pass") assert.False(t, c.fail, "should pass")
} }
func TestStatesDontMatch(t *testing.T) { func TestStatesDontMatch(t *testing.T) {
c := checkStatesMatch("hey", "bye") c := CheckStatesMatch("hey", "bye")
assert.True(t, c.fail, "should fail") assert.True(t, c.fail, "should fail")
} }

View File

@@ -9,7 +9,8 @@ import (
"strings" "strings"
"text/template" "text/template"
ga "github.com/Xevion/gome-assistant" ha "github.com/Xevion/go-ha"
"github.com/Xevion/go-ha/types"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -67,7 +68,7 @@ func toCamelCase(s string) string {
} }
// validateHomeZone verifies that the home zone entity exists and is valid // validateHomeZone verifies that the home zone entity exists and is valid
func validateHomeZone(state ga.State, entityID string) error { func validateHomeZone(state ha.State, entityID string) error {
entity, err := state.Get(entityID) entity, err := state.Get(entityID)
if err != nil { if err != nil {
return fmt.Errorf("home zone entity '%s' not found: %w", entityID, err) return fmt.Errorf("home zone entity '%s' not found: %w", entityID, err)
@@ -98,7 +99,7 @@ func generate(config Config) error {
config.HomeZoneEntityId = "zone.home" config.HomeZoneEntityId = "zone.home"
} }
app, err := ga.NewApp(ga.NewAppRequest{ app, err := ha.NewApp(types.NewAppRequest{
URL: config.URL, URL: config.URL,
HAAuthToken: config.HAAuthToken, HAAuthToken: config.HAAuthToken,
HomeZoneEntityId: config.HomeZoneEntityId, HomeZoneEntityId: config.HomeZoneEntityId,

View File

@@ -5,9 +5,10 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/golang-module/carbon" "github.com/dromara/carbon/v2"
"github.com/Xevion/gome-assistant/internal" "github.com/Xevion/go-ha/internal"
"github.com/Xevion/go-ha/types"
) )
type EntityListener struct { type EntityListener struct {
@@ -16,7 +17,7 @@ type EntityListener struct {
fromState string fromState string
toState string toState string
throttle time.Duration throttle time.Duration
lastRan carbon.Carbon lastRan *carbon.Carbon
betweenStart string betweenStart string
betweenEnd string betweenEnd string
@@ -25,7 +26,7 @@ type EntityListener struct {
delayTimer *time.Timer delayTimer *time.Timer
exceptionDates []time.Time exceptionDates []time.Time
exceptionRanges []timeRange exceptionRanges []types.TimeRange
runOnStartup bool runOnStartup bool
runOnStartupCompleted bool runOnStartupCompleted bool
@@ -126,13 +127,13 @@ func (b elBuilder3) ToState(s string) elBuilder3 {
return b return b
} }
func (b elBuilder3) Duration(s DurationString) elBuilder3 { func (b elBuilder3) Duration(s types.DurationString) elBuilder3 {
d := internal.ParseDuration(string(s)) d := internal.ParseDuration(string(s))
b.entityListener.delay = d b.entityListener.delay = d
return b return b
} }
func (b elBuilder3) Throttle(s DurationString) elBuilder3 { func (b elBuilder3) Throttle(s types.DurationString) elBuilder3 {
d := internal.ParseDuration(string(s)) d := internal.ParseDuration(string(s))
b.entityListener.throttle = d b.entityListener.throttle = d
return b return b
@@ -144,7 +145,7 @@ func (b elBuilder3) ExceptionDates(t time.Time, tl ...time.Time) elBuilder3 {
} }
func (b elBuilder3) ExceptionRange(start, end time.Time) elBuilder3 { func (b elBuilder3) ExceptionRange(start, end time.Time) elBuilder3 {
b.entityListener.exceptionRanges = append(b.entityListener.exceptionRanges, timeRange{start, end}) b.entityListener.exceptionRanges = append(b.entityListener.exceptionRanges, types.TimeRange{Start: start, End: end})
return b return b
} }
@@ -191,7 +192,6 @@ func (b elBuilder3) Build() EntityListener {
return b.entityListener return b.entityListener
} }
/* Functions */
func callEntityListeners(app *App, msgBytes []byte) { func callEntityListeners(app *App, msgBytes []byte) {
msg := stateChangedMsg{} msg := stateChangedMsg{}
_ = json.Unmarshal(msgBytes, &msg) _ = json.Unmarshal(msgBytes, &msg)
@@ -213,31 +213,31 @@ func callEntityListeners(app *App, msgBytes []byte) {
for _, l := range listeners { for _, l := range listeners {
// Check conditions // Check conditions
if c := checkWithinTimeRange(l.betweenStart, l.betweenEnd); c.fail { if c := CheckWithinTimeRange(l.betweenStart, l.betweenEnd); c.fail {
continue continue
} }
if c := checkStatesMatch(l.fromState, data.OldState.State); c.fail { if c := CheckStatesMatch(l.fromState, data.OldState.State); c.fail {
continue continue
} }
if c := checkStatesMatch(l.toState, data.NewState.State); c.fail { if c := CheckStatesMatch(l.toState, data.NewState.State); c.fail {
if l.delayTimer != nil { if l.delayTimer != nil {
l.delayTimer.Stop() l.delayTimer.Stop()
} }
continue continue
} }
if c := checkThrottle(l.throttle, l.lastRan); c.fail { if c := CheckThrottle(l.throttle, l.lastRan); c.fail {
continue continue
} }
if c := checkExceptionDates(l.exceptionDates); c.fail { if c := CheckExceptionDates(l.exceptionDates); c.fail {
continue continue
} }
if c := checkExceptionRanges(l.exceptionRanges); c.fail { if c := CheckExceptionRanges(l.exceptionRanges); c.fail {
continue continue
} }
if c := checkEnabledEntity(app.state, l.enabledEntities); c.fail { if c := CheckEnabledEntity(app.state, l.enabledEntities); c.fail {
continue continue
} }
if c := checkDisabledEntity(app.state, l.disabledEntities); c.fail { if c := CheckDisabledEntity(app.state, l.disabledEntities); c.fail {
continue continue
} }

View File

@@ -5,10 +5,11 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/golang-module/carbon" "github.com/dromara/carbon/v2"
"github.com/Xevion/gome-assistant/internal" "github.com/Xevion/go-ha/internal"
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
"github.com/Xevion/go-ha/types"
) )
type EventListener struct { type EventListener struct {
@@ -17,10 +18,10 @@ type EventListener struct {
betweenStart string betweenStart string
betweenEnd string betweenEnd string
throttle time.Duration throttle time.Duration
lastRan carbon.Carbon lastRan *carbon.Carbon
exceptionDates []time.Time exceptionDates []time.Time
exceptionRanges []timeRange exceptionRanges []types.TimeRange
enabledEntities []internal.EnabledDisabledInfo enabledEntities []internal.EnabledDisabledInfo
disabledEntities []internal.EnabledDisabledInfo disabledEntities []internal.EnabledDisabledInfo
@@ -79,7 +80,7 @@ func (b eventListenerBuilder3) OnlyBefore(end string) eventListenerBuilder3 {
return b return b
} }
func (b eventListenerBuilder3) Throttle(s DurationString) eventListenerBuilder3 { func (b eventListenerBuilder3) Throttle(s types.DurationString) eventListenerBuilder3 {
d := internal.ParseDuration(string(s)) d := internal.ParseDuration(string(s))
b.eventListener.throttle = d b.eventListener.throttle = d
return b return b
@@ -91,7 +92,7 @@ func (b eventListenerBuilder3) ExceptionDates(t time.Time, tl ...time.Time) even
} }
func (b eventListenerBuilder3) ExceptionRange(start, end time.Time) eventListenerBuilder3 { func (b eventListenerBuilder3) ExceptionRange(start, end time.Time) eventListenerBuilder3 {
b.eventListener.exceptionRanges = append(b.eventListener.exceptionRanges, timeRange{start, end}) b.eventListener.exceptionRanges = append(b.eventListener.exceptionRanges, types.TimeRange{Start: start, End: end})
return b return b
} }
@@ -140,7 +141,7 @@ type BaseEventMsg struct {
} }
/* Functions */ /* Functions */
func callEventListeners(app *App, msg ws.ChanMsg) { func callEventListeners(app *App, msg connect.ChannelMessage) {
baseEventMsg := BaseEventMsg{} baseEventMsg := BaseEventMsg{}
_ = json.Unmarshal(msg.Raw, &baseEventMsg) _ = json.Unmarshal(msg.Raw, &baseEventMsg)
listeners, ok := app.eventListeners[baseEventMsg.Event.EventType] listeners, ok := app.eventListeners[baseEventMsg.Event.EventType]
@@ -151,22 +152,22 @@ func callEventListeners(app *App, msg ws.ChanMsg) {
for _, l := range listeners { for _, l := range listeners {
// Check conditions // Check conditions
if c := checkWithinTimeRange(l.betweenStart, l.betweenEnd); c.fail { if c := CheckWithinTimeRange(l.betweenStart, l.betweenEnd); c.fail {
continue continue
} }
if c := checkThrottle(l.throttle, l.lastRan); c.fail { if c := CheckThrottle(l.throttle, l.lastRan); c.fail {
continue continue
} }
if c := checkExceptionDates(l.exceptionDates); c.fail { if c := CheckExceptionDates(l.exceptionDates); c.fail {
continue continue
} }
if c := checkExceptionRanges(l.exceptionRanges); c.fail { if c := CheckExceptionRanges(l.exceptionRanges); c.fail {
continue continue
} }
if c := checkEnabledEntity(app.state, l.enabledEntities); c.fail { if c := CheckEnabledEntity(app.state, l.enabledEntities); c.fail {
continue continue
} }
if c := checkDisabledEntity(app.state, l.disabledEntities); c.fail { if c := CheckDisabledEntity(app.state, l.disabledEntities); c.fail {
continue continue
} }

View File

@@ -7,14 +7,13 @@ import (
"time" "time"
// "example/entities" // Optional import generated entities // "example/entities" // Optional import generated entities
ha "github.com/Xevion/go-ha"
ga "github.com/Xevion/gome-assistant"
) )
//go:generate go run github.com/Xevion/gome-assistant/cmd/generate //go:generate go run github.com/Xevion/go-ha/cmd/generate
func main() { func main() {
app, err := ga.NewApp(ga.NewAppRequest{ app, err := ha.NewApp(ha.NewAppRequest{
URL: "http://192.168.86.67:8123", // Replace with your Home Assistant URL URL: "http://192.168.86.67:8123", // Replace with your Home Assistant URL
HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), HAAuthToken: os.Getenv("HA_AUTH_TOKEN"),
HomeZoneEntityId: "zone.home", HomeZoneEntityId: "zone.home",
@@ -32,25 +31,25 @@ func main() {
slog.Info("Application shutdown complete") slog.Info("Application shutdown complete")
}() }()
pantryDoor := ga. pantryDoor := ha.
NewEntityListener(). NewEntityListener().
EntityIds(entities.BinarySensor.PantryDoor). // Use generated entity constant EntityIds(entities.BinarySensor.PantryDoor). // Use generated entity constant
Call(pantryLights). Call(pantryLights).
Build() Build()
_11pmSched := ga. _11pmSched := ha.
NewDailySchedule(). NewDailySchedule().
Call(lightsOut). Call(lightsOut).
At("23:00"). At("23:00").
Build() Build()
_30minsBeforeSunrise := ga. _30minsBeforeSunrise := ha.
NewDailySchedule(). NewDailySchedule().
Call(sunriseSched). Call(sunriseSched).
Sunrise("-30m"). Sunrise("-30m").
Build() Build()
zwaveEventListener := ga. zwaveEventListener := ha.
NewEventListener(). NewEventListener().
EventTypes("zwave_js_value_notification"). EventTypes("zwave_js_value_notification").
Call(onEvent). Call(onEvent).
@@ -63,7 +62,7 @@ func main() {
app.Start() app.Start()
} }
func pantryLights(service *ga.Service, state ga.State, sensor ga.EntityData) { func pantryLights(service *ha.Service, state ha.State, sensor ha.EntityData) {
l := "light.pantry" l := "light.pantry"
// l := entities.Light.Pantry // Or use generated entity constant // l := entities.Light.Pantry // Or use generated entity constant
if sensor.ToState == "on" { if sensor.ToState == "on" {
@@ -73,18 +72,18 @@ func pantryLights(service *ga.Service, state ga.State, sensor ga.EntityData) {
} }
} }
func onEvent(service *ga.Service, state ga.State, data ga.EventData) { func onEvent(service *ha.Service, state ha.State, data ha.EventData) {
// Since the structure of the event changes depending // Since the structure of the event changes depending
// on the event type, you can Unmarshal the raw json // on the event type, you can Unmarshal the raw json
// into a Go type. If a type for your event doesn't // into a Go type. If a type for your event doesn't
// exist, you can write it yourself! PR's welcome to // exist, you can write it yourself! PR's welcome to
// the eventTypes.go file :) // the eventTypes.go file :)
ev := ga.EventZWaveJSValueNotification{} ev := ha.EventZWaveJSValueNotification{}
json.Unmarshal(data.RawEventJSON, &ev) json.Unmarshal(data.RawEventJSON, &ev)
slog.Info("On event invoked", "event", ev) slog.Info("On event invoked", "event", ev)
} }
func lightsOut(service *ga.Service, state ga.State) { func lightsOut(service *ha.Service, state ha.State) {
// always turn off outside lights // always turn off outside lights
service.Light.TurnOff(entities.Light.OutsideLights) service.Light.TurnOff(entities.Light.OutsideLights)
s, err := state.Get(entities.BinarySensor.LivingRoomMotion) s, err := state.Get(entities.BinarySensor.LivingRoomMotion)
@@ -99,7 +98,7 @@ func lightsOut(service *ga.Service, state ga.State) {
} }
} }
func sunriseSched(service *ga.Service, state ga.State) { func sunriseSched(service *ha.Service, state ha.State) {
service.Light.TurnOn(entities.Light.LivingRoomLamps) service.Light.TurnOn(entities.Light.LivingRoomLamps)
service.Light.TurnOff(entities.Light.ChristmasLights) service.Light.TurnOff(entities.Light.ChristmasLights)
} }

View File

@@ -11,13 +11,13 @@ import (
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
ga "github.com/Xevion/gome-assistant" ha "github.com/Xevion/go-ha"
) )
type ( type (
MySuite struct { MySuite struct {
suite.Suite suite.Suite
app *ga.App app *ha.App
config *Config config *Config
suiteCtx map[string]any suiteCtx map[string]any
} }
@@ -62,7 +62,7 @@ func (s *MySuite) SetupSuite() {
slog.Error("Error unmarshalling config file", "error", err) slog.Error("Error unmarshalling config file", "error", err)
} }
s.app, err = ga.NewApp(ga.NewAppRequest{ s.app, err = ha.NewApp(ha.NewAppRequest{
HAAuthToken: s.config.Hass.HAAuthToken, HAAuthToken: s.config.Hass.HAAuthToken,
IpAddress: s.config.Hass.IpAddress, IpAddress: s.config.Hass.IpAddress,
HomeZoneEntityId: s.config.Hass.HomeZoneEntityId, HomeZoneEntityId: s.config.Hass.HomeZoneEntityId,
@@ -76,13 +76,13 @@ func (s *MySuite) SetupSuite() {
entityId := s.config.Entities.LightEntityId entityId := s.config.Entities.LightEntityId
if entityId != "" { if entityId != "" {
s.suiteCtx["entityCallbackInvoked"] = false s.suiteCtx["entityCallbackInvoked"] = false
etl := ga.NewEntityListener().EntityIds(entityId).Call(s.entityCallback).Build() etl := ha.NewEntityListener().EntityIds(entityId).Call(s.entityCallback).Build()
s.app.RegisterEntityListeners(etl) s.app.RegisterEntityListeners(etl)
} }
s.suiteCtx["dailyScheduleCallbackInvoked"] = false s.suiteCtx["dailyScheduleCallbackInvoked"] = false
runTime := time.Now().Add(1 * time.Minute).Format("15:04") runTime := time.Now().Add(1 * time.Minute).Format("15:04")
dailySchedule := ga.NewDailySchedule().Call(s.dailyScheduleCallback).At(runTime).Build() dailySchedule := ha.NewDailySchedule().Call(s.dailyScheduleCallback).At(runTime).Build()
s.app.RegisterSchedules(dailySchedule) s.app.RegisterSchedules(dailySchedule)
// start GA app // start GA app
@@ -122,13 +122,13 @@ func (s *MySuite) TestSchedule() {
} }
// Capture event after light entity state has changed // Capture event after light entity state has changed
func (s *MySuite) entityCallback(se *ga.Service, st ga.State, e ga.EntityData) { func (s *MySuite) entityCallback(se *ha.Service, st ha.State, e ha.EntityData) {
slog.Info("Entity callback called.", "entity id", e.TriggerEntityId, "from state", e.FromState, "to state", e.ToState) slog.Info("Entity callback called.", "entity id", e.TriggerEntityId, "from state", e.FromState, "to state", e.ToState)
s.suiteCtx["entityCallbackInvoked"] = true s.suiteCtx["entityCallbackInvoked"] = true
} }
// Capture planned daily schedule // Capture planned daily schedule
func (s *MySuite) dailyScheduleCallback(se *ga.Service, st ga.State) { func (s *MySuite) dailyScheduleCallback(se *ha.Service, st ha.State) {
slog.Info("Daily schedule callback called.") slog.Info("Daily schedule callback called.")
s.suiteCtx["dailyScheduleCallbackInvoked"] = true s.suiteCtx["dailyScheduleCallbackInvoked"] = true
} }

View File

@@ -3,22 +3,25 @@ module example
go 1.23 go 1.23
require ( require (
github.com/Xevion/go-ha v0.7.0
github.com/golang-cz/devslog v0.0.8 github.com/golang-cz/devslog v0.0.8
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
github.com/Xevion/gome-assistant v0.2.0
) )
require ( require (
github.com/Workiva/go-datastructures v1.1.5 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gobuffalo/envy v1.10.2 // indirect github.com/gobuffalo/envy v1.10.2 // indirect
github.com/gobuffalo/packd v1.0.2 // indirect github.com/gobuffalo/packd v1.0.2 // indirect
github.com/gobuffalo/packr v1.30.1 // indirect github.com/gobuffalo/packr v1.30.1 // indirect
github.com/golang-module/carbon v1.7.3 // indirect github.com/golang-module/carbon v1.7.1 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/joho/godotenv v1.5.1 // indirect github.com/joho/godotenv v1.5.1 // indirect
github.com/nathan-osman/go-sunrise v1.1.0 // indirect github.com/nathan-osman/go-sunrise v1.1.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/rogpeppe/go-internal v1.11.0 // indirect
golang.org/x/mod v0.9.0 // indirect golang.org/x/mod v0.9.0 // indirect
golang.org/x/net v0.33.0 // indirect
resty.dev/v3 v3.0.0-beta.3 // indirect
) )

View File

@@ -1,4 +1,8 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Workiva/go-datastructures v1.1.5 h1:5YfhQ4ry7bZc2Mc7R0YZyYwpf5c6t1cEFvdAhd6Mkf4=
github.com/Workiva/go-datastructures v1.1.5/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/Xevion/go-ha v0.7.0 h1:jf+ZVSDaw0xjY0TcCA/TodWmAehtm47hDQI5z8XJMQE=
github.com/Xevion/go-ha v0.7.0/go.mod h1:TN+40o0znxEdvR7GQgm5YWMiCEJvsoFbnro2oW38RVU=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
@@ -20,8 +24,8 @@ github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIavi
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
github.com/golang-cz/devslog v0.0.8 h1:53ipA2rC5JzWBWr9qB8EfenvXppenNiF/8DwgtNT5Q4= github.com/golang-cz/devslog v0.0.8 h1:53ipA2rC5JzWBWr9qB8EfenvXppenNiF/8DwgtNT5Q4=
github.com/golang-cz/devslog v0.0.8/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= github.com/golang-cz/devslog v0.0.8/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8=
github.com/golang-module/carbon v1.7.3 h1:p5mUZj7Tg62MblrkF7XEoxVPvhVs20N/kimqsZOQ+/U= github.com/golang-module/carbon v1.7.1 h1:EDPV0YjxeS2kE2cRedfGgDikU6l5D79HB/teHuZDLu8=
github.com/golang-module/carbon v1.7.3/go.mod h1:nUMnXq90Rv8a7h2+YOo2BGKS77Y0w/hMPm4/a8h19N8= github.com/golang-module/carbon v1.7.1/go.mod h1:M/TDTYPp3qWtW68u49dLDJOyGmls6L6BXdo/pyvkMaU=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
@@ -44,6 +48,7 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh
github.com/nathan-osman/go-sunrise v1.1.0 h1:ZqZmtmtzs8Os/DGQYi0YMHpuUqR/iRoJK+wDO0wTCw8= github.com/nathan-osman/go-sunrise v1.1.0 h1:ZqZmtmtzs8Os/DGQYi0YMHpuUqR/iRoJK+wDO0wTCw8=
github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM= github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -63,31 +68,49 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -96,5 +119,5 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/Xevion/gome-assistant v0.2.0 h1:Clo5DrziTdsYydVUTQfroeBVmToMnNHoObr+k6HhbMY= resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
github.com/Xevion/gome-assistant v0.2.0/go.mod h1:jsZUtnxANCP0zB2B7iyy4j7sZohMGop8g+5EB2MER3o= resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=

25
go.mod
View File

@@ -1,22 +1,23 @@
module github.com/Xevion/gome-assistant module github.com/Xevion/go-ha
go 1.21 go 1.23.0
toolchain go1.24.2
require ( require (
github.com/golang-module/carbon v1.7.1 github.com/Workiva/go-datastructures v1.1.5
github.com/gorilla/websocket v1.5.0 github.com/dromara/carbon/v2 v2.6.11
github.com/gorilla/websocket v1.5.3
github.com/nathan-osman/go-sunrise v1.1.0 github.com/nathan-osman/go-sunrise v1.1.0
github.com/stretchr/testify v1.10.0
gopkg.in/yaml.v3 v3.0.1
resty.dev/v3 v3.0.0-beta.3
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gobuffalo/envy v1.10.2 // indirect github.com/kr/pretty v0.1.0 // indirect
github.com/gobuffalo/packd v1.0.2 // indirect
github.com/gobuffalo/packr v1.30.1 // indirect
github.com/joho/godotenv v1.4.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect golang.org/x/net v0.42.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
github.com/stretchr/testify v1.8.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
) )

101
go.sum
View File

@@ -1,91 +1,56 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Workiva/go-datastructures v1.1.5 h1:5YfhQ4ry7bZc2Mc7R0YZyYwpf5c6t1cEFvdAhd6Mkf4=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/Workiva/go-datastructures v1.1.5/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/dromara/carbon/v2 v2.6.11 h1:wnAWZ+sbza1uXw3r05hExNSCaBPFaarWfUvYAX86png=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= github.com/dromara/carbon/v2 v2.6.11/go.mod h1:7GXqCUplwN1s1b4whGk2zX4+g4CMCoDIZzmjlyt0vLY=
github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw=
github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8=
github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg=
github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk=
github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw=
github.com/golang-module/carbon v1.7.1 h1:EDPV0YjxeS2kE2cRedfGgDikU6l5D79HB/teHuZDLu8=
github.com/golang-module/carbon v1.7.1/go.mod h1:M/TDTYPp3qWtW68u49dLDJOyGmls6L6BXdo/pyvkMaU=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/nathan-osman/go-sunrise v1.1.0 h1:ZqZmtmtzs8Os/DGQYi0YMHpuUqR/iRoJK+wDO0wTCw8= github.com/nathan-osman/go-sunrise v1.1.0 h1:ZqZmtmtzs8Os/DGQYi0YMHpuUqR/iRoJK+wDO0wTCw8=
github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM= github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/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 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
resty.dev/v3 v3.0.0-beta.3 h1:3kEwzEgCnnS6Ob4Emlk94t+I/gClyoah7SnNi67lt+E=
resty.dev/v3 v3.0.0-beta.3/go.mod h1:OgkqiPvTDtOuV4MGZuUDhwOpkY8enjOsjjMzeOHefy4=

View File

@@ -0,0 +1,181 @@
// Package websocket is used to interact with the Home Assistant
// websocket API. All HA interaction is done via websocket
// except for cases explicitly called out in http package
// documentation.
package connect
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/url"
"sync"
"time"
"github.com/Xevion/go-ha/internal"
"github.com/gorilla/websocket"
)
var ErrInvalidToken = errors.New("invalid authentication token")
// HAConnection is a wrapper around a websocket connection that provides a mutex for thread safety.
type HAConnection struct {
Conn *websocket.Conn // Note: this is not thread safe except for Close() and WriteControl()
mutex sync.Mutex
}
// WriteMessage writes a message to the websocket connection.
func (w *HAConnection) WriteMessage(msg any) error {
w.mutex.Lock()
defer w.mutex.Unlock()
return w.Conn.WriteJSON(msg)
}
// ReadMessageRaw reads a raw message from the websocket connection.
func ReadMessageRaw(conn *websocket.Conn) ([]byte, error) {
_, msg, err := conn.ReadMessage()
if err != nil {
return nil, err
}
return msg, nil
}
// ReadMessage reads a message from the websocket connection and unmarshals it into the given type.
func ReadMessage[T any](conn *websocket.Conn) (T, error) {
var result T
_, msg, err := conn.ReadMessage()
if err != nil {
return result, err
}
err = json.Unmarshal(msg, &result)
if err != nil {
return result, err
}
return result, nil
}
// ConnectionFromUri creates a new websocket connection from the given base URL and authentication token.
func ConnectionFromUri(baseUrl *url.URL, token string) (*HAConnection, context.Context, context.CancelFunc, error) {
// Build the websocket URL
urlWebsockets := *baseUrl
urlWebsockets.Path = "/api/websocket"
scheme, err := internal.GetEquivalentWebsocketScheme(baseUrl.Scheme)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to build websocket URL: %w", err)
}
urlWebsockets.Scheme = scheme
// Create a short timeout context for the connection only
connCtx, connCtxCancel := context.WithTimeout(context.Background(), time.Second*3)
defer connCtxCancel() // Always cancel the connection context when we're done
// Init websocket connection
dialer := websocket.DefaultDialer
conn, _, err := dialer.DialContext(connCtx, urlWebsockets.String(), nil)
if err != nil {
slog.Error("Failed to connect to websocket. Check URI\n", "url", urlWebsockets)
return nil, nil, nil, err
}
// Read auth_required message
msg, err := ReadMessage[struct {
MsgType string `json:"type"`
}](conn)
if err != nil {
slog.Error("Unknown error creating websocket client\n")
return nil, nil, nil, err
} else if msg.MsgType != "auth_required" {
slog.Error("Expected auth_required message, got", "msgType", msg.MsgType)
return nil, nil, nil, fmt.Errorf("expected auth_required message, got %s", msg.MsgType)
}
// Send auth message
err = SendAuthMessage(conn, connCtx, token)
if err != nil {
slog.Error("Unknown error creating websocket client\n")
return nil, nil, nil, err
}
// Verify auth message was successful
err = VerifyAuthResponse(conn, connCtx)
if err != nil {
slog.Error("Auth token is invalid. Please double check it or create a new token in your Home Assistant profile\n")
return nil, nil, nil, err
}
// Create a new background context for the application lifecycle (no timeout)
appCtx, appCtxCancel := context.WithCancel(context.Background())
return &HAConnection{Conn: conn}, appCtx, appCtxCancel, nil
}
// SendAuthMessage sends an auth message to the websocket connection.
func SendAuthMessage(conn *websocket.Conn, ctx context.Context, token string) error {
type AuthMessage struct {
MsgType string `json:"type"`
AccessToken string `json:"access_token"`
}
err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token})
if err != nil {
return err
}
return nil
}
// VerifyAuthResponse verifies that the auth response is valid.
func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error {
msg, err := ReadMessage[struct {
MsgType string `json:"type"`
Message string `json:"message"`
}](conn)
if err != nil {
return err
}
if msg.MsgType != "auth_ok" {
return ErrInvalidToken
}
return nil
}
func SubscribeToStateChangedEvents(id int64, conn *HAConnection, ctx context.Context) {
SubscribeToEventType("state_changed", conn, ctx, id)
}
// TODO: Instead of using variadic arguments, just use a nillable pointer for the id
func SubscribeToEventType(eventType string, conn *HAConnection, ctx context.Context, id ...int64) {
type SubEvent struct {
Id int64 `json:"id"`
Type string `json:"type"`
EventType string `json:"event_type"`
}
// If no id is provided, generate a new one
var finalId int64
if len(id) == 0 {
finalId = internal.NextId()
} else {
finalId = id[0]
}
e := SubEvent{
Id: finalId,
Type: "subscribe_events",
EventType: eventType,
}
err := conn.WriteMessage(e)
// TODO: Handle errors better
if err != nil {
wrappedErr := fmt.Errorf("error writing to websocket: %w", err)
slog.Error(wrappedErr.Error())
panic(wrappedErr)
}
}

View File

@@ -0,0 +1,70 @@
package connect
import (
"encoding/json"
"log/slog"
"github.com/gorilla/websocket"
)
// BaseMessage is the base message type for all messages sent by the websocket server.
type BaseMessage struct {
Type string `json:"type"`
Id int64 `json:"id"`
Success bool `json:"success"` // not present in all messages
}
type ChannelMessage struct {
Id int64
Type string
Success bool
Raw []byte
}
// ListenWebsocket reads messages from the websocket connection and sends them to the channel.
// It will close the channel if it encounters an error, or if the channel is full, and return.
// It ignores errors in deserialization.
func ListenWebsocket(conn *websocket.Conn, c chan ChannelMessage) {
for {
raw, err := ReadMessageRaw(conn)
if err != nil {
slog.Error("Error reading from websocket", "err", err)
close(c)
break
}
base := BaseMessage{
// default to true for messages that don't include "success" at all
Success: true,
}
err = json.Unmarshal(raw, &base)
if err != nil {
slog.Error("Error unmarshalling message", "err", err, "message", string(raw))
continue
}
if !base.Success {
slog.Warn("Received unsuccessful response", "response", string(raw))
}
// Create a channel message from the raw message
channelMessage := ChannelMessage{
Type: base.Type,
Id: base.Id,
Success: base.Success,
Raw: raw,
}
// Use non-blocking send to avoid hanging on closed channel
select {
case c <- channelMessage:
// Message sent successfully
default:
// Channel is full or closed, break out of loop
slog.Warn("Websocket message channel is full or closed, stopping listener",
"channel_capacity", cap(c),
"channel_length", len(c))
close(c)
return
}
}
}

79
internal/http.go Normal file
View File

@@ -0,0 +1,79 @@
// http is used to interact with the home assistant
// REST API. Currently only used to retrieve state for
// a single entity_id
package internal
import (
"context"
"errors"
"net/url"
"time"
"resty.dev/v3"
)
type HttpClient struct {
client *resty.Client
baseRequest *resty.Request
}
func NewHttpClient(ctx context.Context, baseUrl *url.URL, token string) *HttpClient {
// Shallow copy the URL to avoid modifying the original
u := *baseUrl
u.Path = "/api"
// Create resty client with configuration
client := resty.New().
SetBaseURL(u.String()).
SetTimeout(30*time.Second).
SetRetryCount(3).
SetRetryWaitTime(1*time.Second).
SetRetryMaxWaitTime(5*time.Second).
AddRetryConditions(func(r *resty.Response, err error) bool {
return err != nil || (r.StatusCode() >= 500 && r.StatusCode() != 403)
}).
SetHeader("User-Agent", "go-ha/"+currentVersion).
SetContext(ctx)
return &HttpClient{
client: client,
baseRequest: client.R().
SetContentType("application/json").
SetHeader("Accept", "application/json").
SetAuthToken(token),
}
}
// getRequest returns a new request
func (c *HttpClient) getRequest() *resty.Request {
return c.baseRequest.Clone(c.client.Context())
}
func (c *HttpClient) GetState(entityId string) ([]byte, error) {
resp, err := c.getRequest().Get("/states/" + entityId)
if err != nil {
return nil, errors.New("Error making HTTP request: " + err.Error())
}
if resp.StatusCode() >= 400 {
return nil, errors.New("HTTP error: " + resp.Status() + " - " + string(resp.Bytes()))
}
return resp.Bytes(), nil
}
// GetStates returns the states of all entities.
func (c *HttpClient) GetStates() ([]byte, error) {
resp, err := c.getRequest().Get("/states")
if err != nil {
return nil, errors.New("Error making HTTP request: " + err.Error())
}
if resp.StatusCode() >= 400 {
return nil, errors.New("HTTP error: " + resp.Status() + " - " + string(resp.Bytes()))
}
return resp.Bytes(), nil
}

View File

@@ -1,103 +0,0 @@
// http is used to interact with the home assistant
// REST API. Currently only used to retrieve state for
// a single entity_id
package http
import (
"errors"
"io"
"net/http"
"net/url"
)
type HttpClient struct {
url string
token string
}
func NewHttpClient(url *url.URL, token string) *HttpClient {
// Shallow copy the URL to avoid modifying the original
u := *url
u.Path = "/api"
if u.Scheme == "ws" {
u.Scheme = "http"
}
if u.Scheme == "wss" {
u.Scheme = "https"
}
return &HttpClient{
url: u.String(),
token: token,
}
}
func (c *HttpClient) GetState(entityId string) ([]byte, error) {
resp, err := get(c.url+"/states/"+entityId, c.token)
if err != nil {
return nil, err
}
return resp, nil
}
func (c *HttpClient) States() ([]byte, error) {
resp, err := get(c.url+"/states", c.token)
if err != nil {
return nil, err
}
return resp, nil
}
func get(url, token string) ([]byte, error) {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, errors.New("Error creating HTTP request: " + err.Error())
}
req.Header.Add("Authorization", "Bearer "+token)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, errors.New("Error on response.\n[ERROR] -" + err.Error())
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.New("Error while reading the response bytes:" + err.Error())
}
return body, nil
}
// func post(url string, token string, data any) ([]byte, error) {
// postBody, err := json.Marshal(data)
// if err != nil {
// return nil, err
// }
// req, err := http.NewRequest("GET", url, bytes.NewBuffer(postBody))
// if err != nil {
// return nil, errors.New("Error building post request: " + err.Error())
// }
// req.Header.Add("Authorization", "Bearer "+token)
// client := &http.Client{}
// resp, err := client.Do(req)
// if err != nil {
// return nil, errors.New("Error in post response: " + err.Error())
// }
// defer resp.Body.Close()
// if resp.StatusCode == 401 {
// panic("ERROR: Auth token is invalid. Please double check it or create a new token in your Home Assistant profile")
// }
// body, err := io.ReadAll(resp.Body)
// if err != nil {
// panic(err)
// }
// return body, nil
// }

50
internal/misc.go Normal file
View File

@@ -0,0 +1,50 @@
package internal
import (
"fmt"
"reflect"
"runtime"
"sync/atomic"
)
type EnabledDisabledInfo struct {
Entity string
State string
RunOnError bool
}
var (
currentVersion = "0.7.1"
)
var (
id atomic.Int64 // default value is 0
)
// NextId returns a unique integer (for the given process), often used for providing a uniquely identifiable
// id for a request. This function is thread-safe.
func NextId() int64 {
return id.Add(1)
}
// GetFunctionName returns the name of the function that the interface is a pointer to.
func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}
// GetEquivalentWebsocketScheme returns the equivalent websocket scheme for the given scheme.
// If the scheme is http or https, it returns ws or wss respectively.
// If the scheme is ws or wss, it returns the same scheme.
// If the scheme is not any of the above, it returns an error.
func GetEquivalentWebsocketScheme(scheme string) (string, error) {
switch scheme {
case "http":
return "ws", nil
case "https":
return "wss", nil
case "ws", "wss":
return scheme, nil
default:
return "", fmt.Errorf("unexpected scheme: %s", scheme)
}
}

View File

@@ -3,28 +3,13 @@ package internal
import ( import (
"fmt" "fmt"
"log/slog" "log/slog"
"reflect"
"runtime"
"time" "time"
"github.com/golang-module/carbon" "github.com/dromara/carbon/v2"
) )
type EnabledDisabledInfo struct {
Entity string
State string
RunOnError bool
}
var id int64 = 0
func GetId() int64 {
id += 1
return id
}
// Parses a HH:MM string. // Parses a HH:MM string.
func ParseTime(s string) carbon.Carbon { func ParseTime(s string) *carbon.Carbon {
t, err := time.Parse("15:04", s) t, err := time.Parse("15:04", s)
if err != nil { if err != nil {
parsingErr := fmt.Errorf("failed to parse time string \"%s\"; format must be HH:MM.: %w", s, err) parsingErr := fmt.Errorf("failed to parse time string \"%s\"; format must be HH:MM.: %w", s, err)
@@ -43,7 +28,3 @@ func ParseDuration(s string) time.Duration {
} }
return d return d
} }
func GetFunctionName(i interface{}) string {
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
}

View File

@@ -1,87 +0,0 @@
package priority_queue
import (
"container/heap"
"errors"
)
// PriorityQueue represents the queue
type PriorityQueue struct {
itemHeap *itemHeap
lookup map[interface{}]*item
}
// New initializes an empty priority queue.
func New() PriorityQueue {
return PriorityQueue{
itemHeap: &itemHeap{},
lookup: make(map[interface{}]*item),
}
}
// Len returns the number of elements in the queue.
func (p *PriorityQueue) Len() int {
return p.itemHeap.Len()
}
// Insert inserts a new element into the queue. No action is performed on duplicate elements.
func (p *PriorityQueue) Insert(v interface{ Hash() string }, priority float64) {
_, ok := p.lookup[v.Hash()]
if ok {
return
}
newItem := &item{
value: v,
priority: priority,
}
heap.Push(p.itemHeap, newItem)
p.lookup[v.Hash()] = newItem
}
// Pop removes the element with the highest priority from the queue and returns it.
// In case of an empty queue, an error is returned.
func (p *PriorityQueue) Pop() (interface{}, error) {
if len(*p.itemHeap) == 0 {
return nil, errors.New("empty queue")
}
item := heap.Pop(p.itemHeap).(*item)
delete(p.lookup, item.value.(interface{ Hash() string }).Hash())
return item.value, nil
}
type itemHeap []*item
type item struct {
value interface{}
priority float64
index int
}
func (ih *itemHeap) Len() int {
return len(*ih)
}
func (ih *itemHeap) Less(i, j int) bool {
return (*ih)[i].priority < (*ih)[j].priority
}
func (ih *itemHeap) Swap(i, j int) {
(*ih)[i], (*ih)[j] = (*ih)[j], (*ih)[i]
(*ih)[i].index = i
(*ih)[j].index = j
}
func (ih *itemHeap) Push(x interface{}) {
it := x.(*item)
it.index = len(*ih)
*ih = append(*ih, it)
}
func (ih *itemHeap) Pop() interface{} {
old := *ih
item := old[len(old)-1]
*ih = old[0 : len(old)-1]
return item
}

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type AdaptiveLighting struct { type AdaptiveLighting struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Set manual control for an adaptive lighting entity. // Set manual control for an adaptive lighting entity.
func (al AdaptiveLighting) SetManualControl(entityId string, enabled bool) error { func (al AdaptiveLighting) SetManualControl(entityId string, enabled bool) error {
req := NewBaseServiceRequest("") req := NewBaseServiceRequest("")

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type AlarmControlPanel struct { type AlarmControlPanel struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Send the alarm the command for arm away. // Send the alarm the command for arm away.
// Takes an entityId and an optional // Takes an entityId and an optional
// map that is translated into service_data. // map that is translated into service_data.

View File

@@ -1,18 +1,14 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
"github.com/Xevion/gome-assistant/types" "github.com/Xevion/go-ha/types"
) )
/* Structs */
type Climate struct { type Climate struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
func (c Climate) SetFanMode(entityId string, fanMode string) error { func (c Climate) SetFanMode(entityId string, fanMode string) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)
req.Domain = "climate" req.Domain = "climate"

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type Cover struct { type Cover struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Close all or specified cover. Takes an entityId. // Close all or specified cover. Takes an entityId.
func (c Cover) Close(entityId string) error { func (c Cover) Close(entityId string) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)

View File

@@ -1,12 +1,12 @@
package services package services
import ( import (
"github.com/Xevion/gome-assistant/internal" "github.com/Xevion/go-ha/internal"
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
type Event struct { type Event struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
// Fire an event // Fire an event
@@ -17,13 +17,11 @@ type FireEventRequest struct {
EventData map[string]any `json:"event_data,omitempty"` EventData map[string]any `json:"event_data,omitempty"`
} }
/* Public API */
// Fire an event. Takes an event type and an optional map that is sent // Fire an event. Takes an event type and an optional map that is sent
// as `event_data`. // as `event_data`.
func (e Event) Fire(eventType string, eventData ...map[string]any) error { func (e Event) Fire(eventType string, eventData ...map[string]any) error {
req := FireEventRequest{ req := FireEventRequest{
Id: internal.GetId(), Id: internal.NextId(),
Type: "fire_event", Type: "fire_event",
} }

View File

@@ -1,11 +1,11 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
type HomeAssistant struct { type HomeAssistant struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
// TurnOn a Home Assistant entity. Takes an entityId and an optional // TurnOn a Home Assistant entity. Takes an entityId and an optional

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type InputBoolean struct { type InputBoolean struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
func (ib InputBoolean) TurnOn(entityId string) error { func (ib InputBoolean) TurnOn(entityId string) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)
req.Domain = "input_boolean" req.Domain = "input_boolean"

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type InputButton struct { type InputButton struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
func (ib InputButton) Press(entityId string) error { func (ib InputButton) Press(entityId string) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)
req.Domain = "input_button" req.Domain = "input_button"

View File

@@ -4,17 +4,13 @@ import (
"fmt" "fmt"
"time" "time"
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type InputDatetime struct { type InputDatetime struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
func (ib InputDatetime) Set(entityId string, value time.Time) error { func (ib InputDatetime) Set(entityId string, value time.Time) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)
req.Domain = "input_datetime" req.Domain = "input_datetime"

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type InputNumber struct { type InputNumber struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
func (ib InputNumber) Set(entityId string, value float32) error { func (ib InputNumber) Set(entityId string, value float32) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)
req.Domain = "input_number" req.Domain = "input_number"

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type InputText struct { type InputText struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
func (ib InputText) Set(entityId string, value string) error { func (ib InputText) Set(entityId string, value string) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)
req.Domain = "input_text" req.Domain = "input_text"

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type Light struct { type Light struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// TurnOn a light entity. Takes an entityId and an optional // TurnOn a light entity. Takes an entityId and an optional
// map that is translated into service_data. // map that is translated into service_data.
func (l Light) TurnOn(entityId string, serviceData ...map[string]any) error { func (l Light) TurnOn(entityId string, serviceData ...map[string]any) error {

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type Lock struct { type Lock struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Lock a lock entity. Takes an entityId and an optional // Lock a lock entity. Takes an entityId and an optional
// map that is translated into service_data. // map that is translated into service_data.
func (l Lock) Lock(entityId string, serviceData ...map[string]any) error { func (l Lock) Lock(entityId string, serviceData ...map[string]any) error {

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type MediaPlayer struct { type MediaPlayer struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Send the media player the command to clear players playlist. // Send the media player the command to clear players playlist.
// Takes an entityId. // Takes an entityId.
func (mp MediaPlayer) ClearPlaylist(entityId string) error { func (mp MediaPlayer) ClearPlaylist(entityId string) error {

View File

@@ -1,12 +1,12 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
"github.com/Xevion/gome-assistant/types" "github.com/Xevion/go-ha/types"
) )
type Notify struct { type Notify struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
// Notify sends a notification. Takes a types.NotifyRequest. // Notify sends a notification. Takes a types.NotifyRequest.

View File

@@ -1,11 +1,11 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
type Number struct { type Number struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
func (ib Number) SetValue(entityId string, value float32) error { func (ib Number) SetValue(entityId string, value float32) error {

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type Scene struct { type Scene struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Apply a scene. Takes map that is translated into service_data. // Apply a scene. Takes map that is translated into service_data.
func (s Scene) Apply(serviceData ...map[string]any) error { func (s Scene) Apply(serviceData ...map[string]any) error {
req := NewBaseServiceRequest("") req := NewBaseServiceRequest("")

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type Script struct { type Script struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Reload a script that was created in the HA UI. // Reload a script that was created in the HA UI.
func (s Script) Reload(entityId string) error { func (s Script) Reload(entityId string) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)

View File

@@ -1,8 +1,8 @@
package services package services
import ( import (
"github.com/Xevion/gome-assistant/internal" "github.com/Xevion/go-ha/internal"
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
func BuildService[ func BuildService[
@@ -29,7 +29,7 @@ func BuildService[
Timer | Timer |
Vacuum | Vacuum |
ZWaveJS, ZWaveJS,
](conn *ws.WebsocketWriter) *T { ](conn *connect.HAConnection) *T {
return &T{conn: conn} return &T{conn: conn}
} }
@@ -45,13 +45,15 @@ type BaseServiceRequest struct {
} }
func NewBaseServiceRequest(entityId string) BaseServiceRequest { func NewBaseServiceRequest(entityId string) BaseServiceRequest {
id := internal.GetId() id := internal.NextId()
bsr := BaseServiceRequest{ request := BaseServiceRequest{
Id: id, Id: id,
RequestType: "call_service", RequestType: "call_service",
} }
if entityId != "" { if entityId != "" {
bsr.Target.EntityId = entityId request.Target.EntityId = entityId
} }
return bsr
return request
} }

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type Switch struct { type Switch struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
func (s Switch) TurnOn(entityId string) error { func (s Switch) TurnOn(entityId string) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)
req.Domain = "switch" req.Domain = "switch"

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type Timer struct { type Timer struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// See https://www.home-assistant.io/integrations/timer/#action-timerstart // See https://www.home-assistant.io/integrations/timer/#action-timerstart
func (t Timer) Start(entityId string, duration string) error { func (t Timer) Start(entityId string, duration string) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type TTS struct { type TTS struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Remove all text-to-speech cache files and RAM cache. // Remove all text-to-speech cache files and RAM cache.
func (tts TTS) ClearCache() error { func (tts TTS) ClearCache() error {
req := NewBaseServiceRequest("") req := NewBaseServiceRequest("")

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type Vacuum struct { type Vacuum struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// Tell the vacuum cleaner to do a spot clean-up. // Tell the vacuum cleaner to do a spot clean-up.
// Takes an entityId. // Takes an entityId.
func (v Vacuum) CleanSpot(entityId string) error { func (v Vacuum) CleanSpot(entityId string) error {

View File

@@ -1,17 +1,13 @@
package services package services
import ( import (
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/connect"
) )
/* Structs */
type ZWaveJS struct { type ZWaveJS struct {
conn *ws.WebsocketWriter conn *connect.HAConnection
} }
/* Public API */
// ZWaveJS bulk_set_partial_config_parameters service. // ZWaveJS bulk_set_partial_config_parameters service.
func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, value any) error { func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, value any) error {
req := NewBaseServiceRequest(entityId) req := NewBaseServiceRequest(entityId)

View File

@@ -1,58 +0,0 @@
package websocket
import (
"encoding/json"
"log/slog"
"github.com/gorilla/websocket"
)
type BaseMessage struct {
Type string `json:"type"`
Id int64 `json:"id"`
Success bool `json:"success"`
}
type ChanMsg struct {
Id int64
Type string
Success bool
Raw []byte
}
func ListenWebsocket(conn *websocket.Conn, c chan ChanMsg) {
for {
bytes, err := ReadMessage(conn)
if err != nil {
slog.Error("Error reading from websocket", "err", err)
close(c)
break
}
base := BaseMessage{
// default to true for messages that don't include "success" at all
Success: true,
}
_ = json.Unmarshal(bytes, &base)
if !base.Success {
slog.Warn("Received unsuccessful response", "response", string(bytes))
}
chanMsg := ChanMsg{
Type: base.Type,
Id: base.Id,
Success: base.Success,
Raw: bytes,
}
// Use non-blocking send to avoid hanging on closed channel
select {
case c <- chanMsg:
// Message sent successfully
default:
// Channel is full or closed, break out of loop
slog.Warn("Websocket message channel is full or closed, stopping listener")
close(c)
return
}
}
}

View File

@@ -1,159 +0,0 @@
// Package websocket is used to interact with the Home Assistant
// websocket API. All HA interaction is done via websocket
// except for cases explicitly called out in http package
// documentation.
package websocket
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/url"
"sync"
"time"
"github.com/gorilla/websocket"
i "github.com/Xevion/gome-assistant/internal"
)
var ErrInvalidToken = errors.New("invalid authentication token")
type AuthMessage struct {
MsgType string `json:"type"`
AccessToken string `json:"access_token"`
}
type WebsocketWriter struct {
Conn *websocket.Conn
mutex sync.Mutex
}
func (w *WebsocketWriter) WriteMessage(msg any) error {
w.mutex.Lock()
defer w.mutex.Unlock()
return w.Conn.WriteJSON(msg)
}
func ReadMessage(conn *websocket.Conn) ([]byte, error) {
_, msg, err := conn.ReadMessage()
if err != nil {
return []byte{}, err
}
return msg, nil
}
func ConnectionFromUri(baseURL *url.URL, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) {
ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*3)
// Shallow copy the URL to avoid modifying the original
urlWebsockets := *baseURL
urlWebsockets.Path = "/api/websocket"
if baseURL.Scheme == "http" {
urlWebsockets.Scheme = "ws"
}
if baseURL.Scheme == "https" {
urlWebsockets.Scheme = "wss"
}
// Init websocket connection
dialer := websocket.DefaultDialer
conn, _, err := dialer.DialContext(ctx, urlWebsockets.String(), nil)
if err != nil {
ctxCancel()
slog.Error("Failed to connect to websocket. Check URI\n", "url", urlWebsockets)
return nil, nil, nil, err
}
// Read auth_required message
_, err = ReadMessage(conn)
if err != nil {
ctxCancel()
slog.Error("Unknown error creating websocket client\n")
return nil, nil, nil, err
}
// Send auth message
err = SendAuthMessage(conn, ctx, authToken)
if err != nil {
ctxCancel()
slog.Error("Unknown error creating websocket client\n")
return nil, nil, nil, err
}
// Verify auth message was successful
err = VerifyAuthResponse(conn, ctx)
if err != nil {
ctxCancel()
slog.Error("Auth token is invalid. Please double check it or create a new token in your Home Assistant profile\n")
return nil, nil, nil, err
}
return conn, ctx, ctxCancel, nil
}
func SendAuthMessage(conn *websocket.Conn, ctx context.Context, token string) error {
err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token})
if err != nil {
return err
}
return nil
}
type authResponse struct {
MsgType string `json:"type"`
Message string `json:"message"`
}
func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error {
msg, err := ReadMessage(conn)
if err != nil {
return err
}
var authResp authResponse
err = json.Unmarshal(msg, &authResp)
if err != nil {
return err
}
if authResp.MsgType != "auth_ok" {
return ErrInvalidToken
}
return nil
}
type SubEvent struct {
Id int64 `json:"id"`
Type string `json:"type"`
EventType string `json:"event_type"`
}
func SubscribeToStateChangedEvents(id int64, conn *WebsocketWriter, ctx context.Context) {
SubscribeToEventType("state_changed", conn, ctx, id)
}
func SubscribeToEventType(eventType string, conn *WebsocketWriter, ctx context.Context, id ...int64) {
var finalId int64
if len(id) == 0 {
finalId = i.GetId()
} else {
finalId = id[0]
}
e := SubEvent{
Id: finalId,
Type: "subscribe_events",
EventType: eventType,
}
err := conn.WriteMessage(e)
if err != nil {
wrappedErr := fmt.Errorf("error writing to websocket: %w", err)
slog.Error(wrappedErr.Error())
panic(wrappedErr)
}
// m, _ := ReadMessage(conn, ctx)
// log.Default().Println(string(m))
}

View File

@@ -5,7 +5,8 @@ import (
"log/slog" "log/slog"
"time" "time"
"github.com/Xevion/gome-assistant/internal" "github.com/Xevion/go-ha/internal"
"github.com/Xevion/go-ha/types"
) )
type IntervalCallback func(*Service, State) type IntervalCallback func(*Service, State)
@@ -13,12 +14,12 @@ type IntervalCallback func(*Service, State)
type Interval struct { type Interval struct {
frequency time.Duration frequency time.Duration
callback IntervalCallback callback IntervalCallback
startTime TimeString startTime types.TimeString
endTime TimeString endTime types.TimeString
nextRunTime time.Time nextRunTime time.Time
exceptionDates []time.Time exceptionDates []time.Time
exceptionRanges []timeRange exceptionRanges []types.TimeRange
enabledEntities []internal.EnabledDisabledInfo enabledEntities []internal.EnabledDisabledInfo
disabledEntities []internal.EnabledDisabledInfo disabledEntities []internal.EnabledDisabledInfo
@@ -57,12 +58,12 @@ func (i Interval) String() string {
return fmt.Sprintf("Interval{ call %q every %s%s%s }", return fmt.Sprintf("Interval{ call %q every %s%s%s }",
internal.GetFunctionName(i.callback), internal.GetFunctionName(i.callback),
i.frequency, i.frequency,
formatStartOrEndString(i.startTime /* isStart = */, true), formatStartOrEndString(i.startTime, true),
formatStartOrEndString(i.endTime /* isStart = */, false), formatStartOrEndString(i.endTime, false),
) )
} }
func formatStartOrEndString(s TimeString, isStart bool) string { func formatStartOrEndString(s types.TimeString, isStart bool) string {
if s == "00:00" { if s == "00:00" {
return "" return ""
} }
@@ -79,20 +80,20 @@ func (ib intervalBuilder) Call(callback IntervalCallback) intervalBuilderCall {
} }
// Takes a DurationString ("2h", "5m", etc) to set the frequency of the interval. // Takes a DurationString ("2h", "5m", etc) to set the frequency of the interval.
func (ib intervalBuilderCall) Every(s DurationString) intervalBuilderEnd { func (ib intervalBuilderCall) Every(s types.DurationString) intervalBuilderEnd {
d := internal.ParseDuration(string(s)) d := internal.ParseDuration(string(s))
ib.interval.frequency = d ib.interval.frequency = d
return intervalBuilderEnd(ib) return intervalBuilderEnd(ib)
} }
// Takes a TimeString ("HH:MM") when this interval will start running for the day. // Takes a TimeString ("HH:MM") when this interval will start running for the day.
func (ib intervalBuilderEnd) StartingAt(s TimeString) intervalBuilderEnd { func (ib intervalBuilderEnd) StartingAt(s types.TimeString) intervalBuilderEnd {
ib.interval.startTime = s ib.interval.startTime = s
return ib return ib
} }
// Takes a TimeString ("HH:MM") when this interval will stop running for the day. // Takes a TimeString ("HH:MM") when this interval will stop running for the day.
func (ib intervalBuilderEnd) EndingAt(s TimeString) intervalBuilderEnd { func (ib intervalBuilderEnd) EndingAt(s types.TimeString) intervalBuilderEnd {
ib.interval.endTime = s ib.interval.endTime = s
return ib return ib
} }
@@ -103,7 +104,10 @@ func (ib intervalBuilderEnd) ExceptionDates(t time.Time, tl ...time.Time) interv
} }
func (ib intervalBuilderEnd) ExceptionRange(start, end time.Time) intervalBuilderEnd { func (ib intervalBuilderEnd) ExceptionRange(start, end time.Time) intervalBuilderEnd {
ib.interval.exceptionRanges = append(ib.interval.exceptionRanges, timeRange{start, end}) ib.interval.exceptionRanges = append(
ib.interval.exceptionRanges,
types.TimeRange{Start: start, End: end},
)
return ib return ib
} }
@@ -184,34 +188,37 @@ func runIntervals(a *App) {
} }
func (i Interval) maybeRunCallback(a *App) { func (i Interval) maybeRunCallback(a *App) {
if c := checkStartEndTime(i.startTime /* isStart = */, true); c.fail { if c := CheckStartEndTime(i.startTime /* isStart = */, true); c.fail {
return return
} }
if c := checkStartEndTime(i.endTime /* isStart = */, false); c.fail { if c := CheckStartEndTime(i.endTime /* isStart = */, false); c.fail {
return return
} }
if c := checkExceptionDates(i.exceptionDates); c.fail { if c := CheckExceptionDates(i.exceptionDates); c.fail {
return return
} }
if c := checkExceptionRanges(i.exceptionRanges); c.fail { if c := CheckExceptionRanges(i.exceptionRanges); c.fail {
return return
} }
if c := checkEnabledEntity(a.state, i.enabledEntities); c.fail { if c := CheckEnabledEntity(a.state, i.enabledEntities); c.fail {
return return
} }
if c := checkDisabledEntity(a.state, i.disabledEntities); c.fail { if c := CheckDisabledEntity(a.state, i.disabledEntities); c.fail {
return return
} }
go i.callback(a.service, a.state) go i.callback(a.service, a.state)
} }
func popInterval(a *App) Interval { func popInterval(a *App) Interval {
i, _ := a.intervals.Pop() i, _ := a.intervals.Get(1)
return i.(Interval) return i[0].(Item).Value.(Interval)
} }
func requeueInterval(a *App, i Interval) { func requeueInterval(a *App, i Interval) {
i.nextRunTime = i.nextRunTime.Add(i.frequency) i.nextRunTime = i.nextRunTime.Add(i.frequency)
a.intervals.Insert(i, float64(i.nextRunTime.Unix())) a.intervals.Put(Item{
Value: i,
Priority: float64(i.nextRunTime.Unix()),
})
} }

View File

@@ -5,8 +5,9 @@ import (
"log/slog" "log/slog"
"time" "time"
"github.com/Xevion/gome-assistant/internal" "github.com/Xevion/go-ha/internal"
"github.com/golang-module/carbon" "github.com/Xevion/go-ha/types"
"github.com/dromara/carbon/v2"
) )
type ScheduleCallback func(*Service, State) type ScheduleCallback func(*Service, State)
@@ -22,7 +23,7 @@ type DailySchedule struct {
isSunrise bool isSunrise bool
isSunset bool isSunset bool
sunOffset DurationString sunOffset types.DurationString
exceptionDates []time.Time exceptionDates []time.Time
allowlistDates []time.Time allowlistDates []time.Time
@@ -84,7 +85,7 @@ func (sb scheduleBuilderCall) At(s string) scheduleBuilderEnd {
// Sunrise takes an optional duration string that is passed to time.ParseDuration. // Sunrise takes an optional duration string that is passed to time.ParseDuration.
// Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration // Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration
// for full list. // for full list.
func (sb scheduleBuilderCall) Sunrise(offset ...DurationString) scheduleBuilderEnd { func (sb scheduleBuilderCall) Sunrise(offset ...types.DurationString) scheduleBuilderEnd {
sb.schedule.isSunrise = true sb.schedule.isSunrise = true
if len(offset) > 0 { if len(offset) > 0 {
sb.schedule.sunOffset = offset[0] sb.schedule.sunOffset = offset[0]
@@ -95,7 +96,7 @@ func (sb scheduleBuilderCall) Sunrise(offset ...DurationString) scheduleBuilderE
// Sunset takes an optional duration string that is passed to time.ParseDuration. // Sunset takes an optional duration string that is passed to time.ParseDuration.
// Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration // Examples include "-1.5h", "30m", etc. See https://pkg.go.dev/time#ParseDuration
// for full list. // for full list.
func (sb scheduleBuilderCall) Sunset(offset ...DurationString) scheduleBuilderEnd { func (sb scheduleBuilderCall) Sunset(offset ...types.DurationString) scheduleBuilderEnd {
sb.schedule.isSunset = true sb.schedule.isSunset = true
if len(offset) > 0 { if len(offset) > 0 {
sb.schedule.sunOffset = offset[0] sb.schedule.sunOffset = offset[0]
@@ -192,29 +193,29 @@ func runSchedules(a *App) {
} }
func (s DailySchedule) maybeRunCallback(a *App) { func (s DailySchedule) maybeRunCallback(a *App) {
if c := checkExceptionDates(s.exceptionDates); c.fail { if c := CheckExceptionDates(s.exceptionDates); c.fail {
return return
} }
if c := checkAllowlistDates(s.allowlistDates); c.fail { if c := CheckAllowlistDates(s.allowlistDates); c.fail {
return return
} }
if c := checkEnabledEntity(a.state, s.enabledEntities); c.fail { if c := CheckEnabledEntity(a.state, s.enabledEntities); c.fail {
return return
} }
if c := checkDisabledEntity(a.state, s.disabledEntities); c.fail { if c := CheckDisabledEntity(a.state, s.disabledEntities); c.fail {
return return
} }
go s.callback(a.service, a.state) go s.callback(a.service, a.state)
} }
func popSchedule(a *App) DailySchedule { func popSchedule(a *App) DailySchedule {
_sched, _ := a.schedules.Pop() _sched, _ := a.schedules.Get(1)
return _sched.(DailySchedule) return _sched[0].(Item).Value.(DailySchedule)
} }
func requeueSchedule(a *App, s DailySchedule) { func requeueSchedule(a *App, s DailySchedule) {
if s.isSunrise || s.isSunset { if s.isSunrise || s.isSunset {
var nextSunTime carbon.Carbon var nextSunTime *carbon.Carbon
// "0s" is default value // "0s" is default value
if s.sunOffset != "0s" { if s.sunOffset != "0s" {
nextSunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset) nextSunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset)
@@ -222,10 +223,13 @@ func requeueSchedule(a *App, s DailySchedule) {
nextSunTime = getNextSunRiseOrSet(a, s.isSunrise) nextSunTime = getNextSunRiseOrSet(a, s.isSunrise)
} }
s.nextRunTime = nextSunTime.Carbon2Time() s.nextRunTime = nextSunTime.StdTime()
} else { } else {
s.nextRunTime = carbon.Time2Carbon(s.nextRunTime).AddDay().Carbon2Time() s.nextRunTime = carbon.CreateFromStdTime(s.nextRunTime).AddDay().StdTime()
} }
a.schedules.Insert(s, float64(s.nextRunTime.Unix())) a.schedules.Put(Item{
Value: s,
Priority: float64(s.nextRunTime.Unix()),
})
} }

View File

@@ -1,8 +1,8 @@
package gomeassistant package gomeassistant
import ( import (
"github.com/Xevion/gome-assistant/internal/services" "github.com/Xevion/go-ha/internal/connect"
ws "github.com/Xevion/gome-assistant/internal/websocket" "github.com/Xevion/go-ha/internal/services"
) )
type Service struct { type Service struct {
@@ -31,7 +31,7 @@ type Service struct {
ZWaveJS *services.ZWaveJS ZWaveJS *services.ZWaveJS
} }
func newService(conn *ws.WebsocketWriter) *Service { func newService(conn *connect.HAConnection) *Service {
return &Service{ return &Service{
AdaptiveLighting: services.BuildService[services.AdaptiveLighting](conn), AdaptiveLighting: services.BuildService[services.AdaptiveLighting](conn),
AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn), AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn),

View File

@@ -6,16 +6,17 @@ import (
"strings" "strings"
"time" "time"
"github.com/golang-module/carbon" "github.com/dromara/carbon/v2"
"github.com/Xevion/gome-assistant/internal/http" "github.com/Xevion/go-ha/internal"
"github.com/Xevion/go-ha/types"
) )
type State interface { type State interface {
AfterSunrise(...DurationString) bool AfterSunrise(...types.DurationString) bool
BeforeSunrise(...DurationString) bool BeforeSunrise(...types.DurationString) bool
AfterSunset(...DurationString) bool AfterSunset(...types.DurationString) bool
BeforeSunset(...DurationString) bool BeforeSunset(...types.DurationString) bool
ListEntities() ([]EntityState, error) ListEntities() ([]EntityState, error)
Get(entityId string) (EntityState, error) Get(entityId string) (EntityState, error)
Equals(entityId, state string) (bool, error) Equals(entityId, state string) (bool, error)
@@ -23,7 +24,7 @@ type State interface {
// State is used to retrieve state from Home Assistant. // State is used to retrieve state from Home Assistant.
type StateImpl struct { type StateImpl struct {
httpClient *http.HttpClient httpClient *internal.HttpClient
latitude float64 latitude float64
longitude float64 longitude float64
} }
@@ -35,7 +36,7 @@ type EntityState struct {
LastChanged time.Time `json:"last_changed"` LastChanged time.Time `json:"last_changed"`
} }
func newState(c *http.HttpClient, homeZoneEntityId string) (*StateImpl, error) { func newState(c *internal.HttpClient, homeZoneEntityId string) (*StateImpl, error) {
state := &StateImpl{httpClient: c} state := &StateImpl{httpClient: c}
// Ensure the zone exists and has required attributes // Ensure the zone exists and has required attributes
@@ -82,7 +83,7 @@ func (s *StateImpl) Get(entityId string) (EntityState, error) {
// ListEntities returns a list of all entities in Home Assistant. // ListEntities returns a list of all entities in Home Assistant.
// see rest documentation for more details: https://developers.home-assistant.io/docs/api/rest/#actions // see rest documentation for more details: https://developers.home-assistant.io/docs/api/rest/#actions
func (s *StateImpl) ListEntities() ([]EntityState, error) { func (s *StateImpl) ListEntities() ([]EntityState, error) {
resp, err := s.httpClient.States() resp, err := s.httpClient.GetStates()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -99,20 +100,20 @@ func (s *StateImpl) Equals(entityId string, expectedState string) (bool, error)
return currentState.State == expectedState, nil return currentState.State == expectedState, nil
} }
func (s *StateImpl) BeforeSunrise(offset ...DurationString) bool { func (s *StateImpl) BeforeSunrise(offset ...types.DurationString) bool {
sunrise := getSunriseSunset(s /* sunrise = */, true, carbon.Now(), offset...) sunrise := getSunriseSunset(s /* sunrise = */, true, carbon.Now(), offset...)
return carbon.Now().Lt(sunrise) return carbon.Now().Lt(sunrise)
} }
func (s *StateImpl) AfterSunrise(offset ...DurationString) bool { func (s *StateImpl) AfterSunrise(offset ...types.DurationString) bool {
return !s.BeforeSunrise(offset...) return !s.BeforeSunrise(offset...)
} }
func (s *StateImpl) BeforeSunset(offset ...DurationString) bool { func (s *StateImpl) BeforeSunset(offset ...types.DurationString) bool {
sunset := getSunriseSunset(s /* sunrise = */, false, carbon.Now(), offset...) sunset := getSunriseSunset(s /* sunrise = */, false, carbon.Now(), offset...)
return carbon.Now().Lt(sunset) return carbon.Now().Lt(sunset)
} }
func (s *StateImpl) AfterSunset(offset ...DurationString) bool { func (s *StateImpl) AfterSunset(offset ...types.DurationString) bool {
return !s.BeforeSunset(offset...) return !s.BeforeSunset(offset...)
} }

35
types/app.go Normal file
View File

@@ -0,0 +1,35 @@
package types
// NewAppRequest contains the configuration for creating a new App instance.
type NewAppRequest struct {
// Required
URL string
// Optional
// Deprecated: use URL instead
// IpAddress of your Home Assistant instance i.e. "localhost"
// or "192.168.86.59" etc.
IpAddress string
// Optional
// Deprecated: use URL instead
// Port number Home Assistant is running on. Defaults to 8123.
Port string
// Required
// Auth token generated in Home Assistant. Used
// to connect to the Websocket API.
HAAuthToken string
// Required
// EntityId of the zone representing your home e.g. "zone.home".
// Used to pull latitude/longitude from Home Assistant
// to calculate sunset/sunrise times.
HomeZoneEntityId string
// Optional
// Whether to use secure connections for http and websockets.
// Setting this to `true` will use `https://` instead of `https://`
// and `wss://` instead of `ws://`.
Secure bool
}

22
types/common.go Normal file
View File

@@ -0,0 +1,22 @@
package types
import "time"
// DurationString represents a duration, such as "2s" or "24h".
// See https://pkg.go.dev/time#ParseDuration for all valid time units.
type DurationString string
// TimeString is a 24-hr format time "HH:MM" such as "07:30".
type TimeString string
// TimeRange represents a time range with start and end times.
type TimeRange struct {
Start time.Time
End time.Time
}
// Item represents a priority queue item with a value and priority.
type Item struct {
Value interface{}
Priority float64
}

View File

@@ -1,4 +1,4 @@
package gomeassistant package types
import "time" import "time"

View File