mirror of
https://github.com/Xevion/go-ha.git
synced 2025-12-08 16:07:19 -06:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e13fd6ab5b | |||
| d281d70d8c | |||
| 8fe6bc0cff | |||
| 9b8ef545a6 | |||
| 102a4e7438 | |||
| d51f6d5946 | |||
| 26b8892ff6 | |||
| 21358b73e1 | |||
| 3d178ad05e |
@@ -85,7 +85,7 @@ The general flow is
|
|||||||
3. Start app
|
3. Start app
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import ga "github.com/Xevion/go-ha"
|
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{
|
||||||
|
|||||||
184
app.go
184
app.go
@@ -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/Workiva/go-datastructures/queue"
|
"github.com/Workiva/go-datastructures/queue"
|
||||||
internal "github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
"github.com/Xevion/go-ha/internal/parse"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidArgs = errors.New("invalid arguments provided")
|
var ErrInvalidArgs = errors.New("invalid arguments provided")
|
||||||
@@ -24,10 +24,9 @@ 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 *internal.HttpClient
|
httpClient *internal.HttpClient
|
||||||
|
|
||||||
@@ -41,10 +40,7 @@ type App struct {
|
|||||||
eventListeners map[string][]*EventListener
|
eventListeners map[string][]*EventListener
|
||||||
}
|
}
|
||||||
|
|
||||||
type Item struct {
|
type Item types.Item
|
||||||
Value interface{}
|
|
||||||
Priority float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mi Item) Compare(other queue.Item) int {
|
func (mi Item) Compare(other queue.Item) int {
|
||||||
if mi.Priority > other.(Item).Priority {
|
if mi.Priority > other.(Item).Priority {
|
||||||
@@ -55,51 +51,6 @@ func (mi Item) Compare(other queue.Item) int {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
|
|
||||||
type timeRange struct {
|
|
||||||
start time.Time
|
|
||||||
end time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
func validateHomeZone(state State, entityID string) error {
|
func validateHomeZone(state State, entityID string) error {
|
||||||
entity, err := state.Get(entityID)
|
entity, err := state.Get(entityID)
|
||||||
@@ -115,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,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
|
||||||
@@ -151,18 +100,14 @@ func NewApp(request NewAppRequest) (*App, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 := internal.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
|
||||||
@@ -175,7 +120,6 @@ 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,
|
||||||
@@ -188,27 +132,27 @@ func NewApp(request NewAppRequest) (*App, error) {
|
|||||||
}, 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 {
|
||||||
// Close websocket connection if it exists
|
// Close websocket connection if it exists
|
||||||
if a.conn != nil {
|
if app.conn != nil {
|
||||||
deadline := time.Now().Add(10 * time.Second)
|
deadline := time.Now().Add(10 * time.Second)
|
||||||
err := a.conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), deadline)
|
err := app.conn.Conn.WriteControl(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""), deadline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Error writing close message", "error", err)
|
slog.Warn("Error writing close message", "error", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
return err
|
||||||
@@ -219,8 +163,8 @@ func (a *App) Close() error {
|
|||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
|
|
||||||
// Cancel context to signal all goroutines to stop
|
// Cancel context to signal all goroutines to stop
|
||||||
if a.ctxCancel != nil {
|
if app.ctxCancel != nil {
|
||||||
a.ctxCancel()
|
app.ctxCancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait a short time for goroutines to finish
|
// Wait a short time for goroutines to finish
|
||||||
@@ -230,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.Put()
|
app.schedules.Put()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,34 +191,34 @@ func (a *App) RegisterSchedules(schedules ...DailySchedule) {
|
|||||||
startTime = startTime.AddDay()
|
startTime = startTime.AddDay()
|
||||||
}
|
}
|
||||||
|
|
||||||
s.nextRunTime = startTime.Carbon2Time()
|
s.nextRunTime = startTime.StdTime()
|
||||||
a.schedules.Put(Item{
|
app.schedules.Put(Item{
|
||||||
Value: s,
|
Value: s,
|
||||||
Priority: float64(startTime.Carbon2Time().Unix()),
|
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 = parse.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.Put(Item{
|
app.intervals.Put(Item{
|
||||||
Value: i,
|
Value: i,
|
||||||
Priority: float64(i.nextRunTime.Unix()),
|
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 == "" {
|
||||||
@@ -283,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()
|
||||||
|
|
||||||
@@ -339,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
|
||||||
@@ -349,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.NextId()
|
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,
|
||||||
@@ -387,8 +331,8 @@ func (a *App) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// entity listeners and event listeners
|
// entity listeners and event listeners
|
||||||
elChan := make(chan ws.ChanMsg, 100) // Add buffer to prevent channel overflow
|
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 {
|
||||||
@@ -397,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
|
||||||
}
|
}
|
||||||
|
|||||||
56
checkers.go
56
checkers.go
@@ -4,21 +4,21 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
"github.com/Xevion/go-ha/internal/parse"
|
"github.com/Xevion/go-ha/types"
|
||||||
"github.com/golang-module/carbon"
|
"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 != "" {
|
||||||
parsedStart := parse.ParseTime(startTime)
|
parsedStart := internal.ParseTime(startTime)
|
||||||
parsedEnd := parse.ParseTime(endTime)
|
parsedEnd := internal.ParseTime(endTime)
|
||||||
|
|
||||||
// check for midnight overlap
|
// check for midnight overlap
|
||||||
if parsedEnd.Lt(parsedStart) { // example turn on night lights when motion from 23:00 to 07:00
|
if parsedEnd.Lt(parsedStart) { // example turn on night lights when motion from 23:00 to 07:00
|
||||||
@@ -35,16 +35,16 @@ func checkWithinTimeRange(startTime, endTime string) conditionCheck {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// otherwise just check individual before/after
|
// otherwise just check individual before/after
|
||||||
} else if startTime != "" && parse.ParseTime(startTime).IsFuture() {
|
} else if startTime != "" && internal.ParseTime(startTime).IsFuture() {
|
||||||
cc.fail = true
|
cc.fail = true
|
||||||
} else if endTime != "" && parse.ParseTime(endTime).IsPast() {
|
} else if endTime != "" && internal.ParseTime(endTime).IsPast() {
|
||||||
cc.fail = true
|
cc.fail = true
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -52,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()) {
|
||||||
@@ -62,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()
|
||||||
@@ -75,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
|
||||||
}
|
}
|
||||||
@@ -87,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
|
||||||
}
|
}
|
||||||
@@ -115,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
|
||||||
}
|
}
|
||||||
@@ -144,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()
|
||||||
@@ -161,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 := parse.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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Xevion/go-ha/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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
ga "github.com/Xevion/go-ha"
|
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,
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-module/carbon"
|
"github.com/dromara/carbon/v2"
|
||||||
|
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
"github.com/Xevion/go-ha/internal/parse"
|
"github.com/Xevion/go-ha/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EntityListener struct {
|
type EntityListener struct {
|
||||||
@@ -17,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
|
||||||
@@ -26,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
|
||||||
@@ -127,14 +127,14 @@ 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 := parse.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 := parse.ParseDuration(string(s))
|
d := internal.ParseDuration(string(s))
|
||||||
b.entityListener.throttle = d
|
b.entityListener.throttle = d
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
@@ -145,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,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)
|
||||||
@@ -214,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-module/carbon"
|
"github.com/dromara/carbon/v2"
|
||||||
|
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
"github.com/Xevion/go-ha/internal/parse"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EventListener struct {
|
type EventListener struct {
|
||||||
@@ -18,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
|
||||||
@@ -80,8 +80,8 @@ 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 := parse.ParseDuration(string(s))
|
d := internal.ParseDuration(string(s))
|
||||||
b.eventListener.throttle = d
|
b.eventListener.throttle = d
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
@@ -92,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,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]
|
||||||
@@ -152,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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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/go-ha"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:generate go run github.com/Xevion/go-ha/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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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/go-ha"
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
17
go.mod
17
go.mod
@@ -1,11 +1,13 @@
|
|||||||
module github.com/Xevion/go-ha
|
module github.com/Xevion/go-ha
|
||||||
|
|
||||||
go 1.21
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Workiva/go-datastructures v1.1.5
|
github.com/Workiva/go-datastructures v1.1.5
|
||||||
github.com/golang-module/carbon v1.7.1
|
github.com/dromara/carbon/v2 v2.6.11
|
||||||
github.com/gorilla/websocket v1.5.0
|
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
|
github.com/stretchr/testify v1.10.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
@@ -14,11 +16,8 @@ require (
|
|||||||
|
|
||||||
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
|
||||||
golang.org/x/net v0.33.0 // indirect
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
71
go.sum
71
go.sum
@@ -1,103 +1,46 @@
|
|||||||
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 h1:5YfhQ4ry7bZc2Mc7R0YZyYwpf5c6t1cEFvdAhd6Mkf4=
|
||||||
github.com/Workiva/go-datastructures v1.1.5/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
github.com/Workiva/go-datastructures v1.1.5/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||||
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/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/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 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||||
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 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/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/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.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
|
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/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/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
|
||||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
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-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/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/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.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
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-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.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
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/sync v0.0.0-20201020160332-67f06af15bc9/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-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-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/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/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-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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
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/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-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
@@ -106,8 +49,6 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
|
|||||||
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=
|
||||||
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=
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// websocket API. All HA interaction is done via websocket
|
// websocket API. All HA interaction is done via websocket
|
||||||
// except for cases explicitly called out in http package
|
// except for cases explicitly called out in http package
|
||||||
// documentation.
|
// documentation.
|
||||||
package websocket
|
package connect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -14,53 +14,66 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
|
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrInvalidToken = errors.New("invalid authentication token")
|
var ErrInvalidToken = errors.New("invalid authentication token")
|
||||||
|
|
||||||
type AuthMessage struct {
|
// HAConnection is a wrapper around a websocket connection that provides a mutex for thread safety.
|
||||||
MsgType string `json:"type"`
|
type HAConnection struct {
|
||||||
AccessToken string `json:"access_token"`
|
Conn *websocket.Conn // Note: this is not thread safe except for Close() and WriteControl()
|
||||||
}
|
|
||||||
|
|
||||||
type WebsocketWriter struct {
|
|
||||||
Conn *websocket.Conn
|
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *WebsocketWriter) WriteMessage(msg any) error {
|
// WriteMessage writes a message to the websocket connection.
|
||||||
|
func (w *HAConnection) WriteMessage(msg any) error {
|
||||||
w.mutex.Lock()
|
w.mutex.Lock()
|
||||||
defer w.mutex.Unlock()
|
defer w.mutex.Unlock()
|
||||||
|
|
||||||
return w.Conn.WriteJSON(msg)
|
return w.Conn.WriteJSON(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadMessage(conn *websocket.Conn) ([]byte, error) {
|
// ReadMessageRaw reads a raw message from the websocket connection.
|
||||||
|
func ReadMessageRaw(conn *websocket.Conn) ([]byte, error) {
|
||||||
_, msg, err := conn.ReadMessage()
|
_, msg, err := conn.ReadMessage()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []byte{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return msg, nil
|
return msg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectionFromUri(baseURL *url.URL, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) {
|
// 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
|
// Create a short timeout context for the connection only
|
||||||
connCtx, connCtxCancel := context.WithTimeout(context.Background(), time.Second*3)
|
connCtx, connCtxCancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||||
defer connCtxCancel() // Always cancel the connection context when we're done
|
defer connCtxCancel() // Always cancel the connection context when we're done
|
||||||
|
|
||||||
// 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
|
// Init websocket connection
|
||||||
dialer := websocket.DefaultDialer
|
dialer := websocket.DefaultDialer
|
||||||
conn, _, err := dialer.DialContext(connCtx, urlWebsockets.String(), nil)
|
conn, _, err := dialer.DialContext(connCtx, urlWebsockets.String(), nil)
|
||||||
@@ -70,14 +83,19 @@ func ConnectionFromUri(baseURL *url.URL, authToken string) (*websocket.Conn, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read auth_required message
|
// Read auth_required message
|
||||||
_, err = ReadMessage(conn)
|
msg, err := ReadMessage[struct {
|
||||||
|
MsgType string `json:"type"`
|
||||||
|
}](conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Unknown error creating websocket client\n")
|
slog.Error("Unknown error creating websocket client\n")
|
||||||
return nil, nil, nil, err
|
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
|
// Send auth message
|
||||||
err = SendAuthMessage(conn, connCtx, authToken)
|
err = SendAuthMessage(conn, connCtx, token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Unknown error creating websocket client\n")
|
slog.Error("Unknown error creating websocket client\n")
|
||||||
return nil, nil, nil, err
|
return nil, nil, nil, err
|
||||||
@@ -93,51 +111,54 @@ func ConnectionFromUri(baseURL *url.URL, authToken string) (*websocket.Conn, con
|
|||||||
// Create a new background context for the application lifecycle (no timeout)
|
// Create a new background context for the application lifecycle (no timeout)
|
||||||
appCtx, appCtxCancel := context.WithCancel(context.Background())
|
appCtx, appCtxCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
return conn, appCtx, appCtxCancel, nil
|
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 {
|
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})
|
err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type authResponse struct {
|
// VerifyAuthResponse verifies that the auth response is valid.
|
||||||
MsgType string `json:"type"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error {
|
func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error {
|
||||||
msg, err := ReadMessage(conn)
|
msg, err := ReadMessage[struct {
|
||||||
|
MsgType string `json:"type"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}](conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var authResp authResponse
|
if msg.MsgType != "auth_ok" {
|
||||||
err = json.Unmarshal(msg, &authResp)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if authResp.MsgType != "auth_ok" {
|
|
||||||
return ErrInvalidToken
|
return ErrInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubEvent struct {
|
func SubscribeToStateChangedEvents(id int64, conn *HAConnection, ctx context.Context) {
|
||||||
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)
|
SubscribeToEventType("state_changed", conn, ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubscribeToEventType(eventType string, conn *WebsocketWriter, ctx context.Context, id ...int64) {
|
// 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
|
var finalId int64
|
||||||
if len(id) == 0 {
|
if len(id) == 0 {
|
||||||
finalId = internal.NextId()
|
finalId = internal.NextId()
|
||||||
@@ -149,12 +170,12 @@ func SubscribeToEventType(eventType string, conn *WebsocketWriter, ctx context.C
|
|||||||
Type: "subscribe_events",
|
Type: "subscribe_events",
|
||||||
EventType: eventType,
|
EventType: eventType,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := conn.WriteMessage(e)
|
err := conn.WriteMessage(e)
|
||||||
|
// TODO: Handle errors better
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wrappedErr := fmt.Errorf("error writing to websocket: %w", err)
|
wrappedErr := fmt.Errorf("error writing to websocket: %w", err)
|
||||||
slog.Error(wrappedErr.Error())
|
slog.Error(wrappedErr.Error())
|
||||||
panic(wrappedErr)
|
panic(wrappedErr)
|
||||||
}
|
}
|
||||||
// m, _ := ReadMessage(conn, ctx)
|
|
||||||
// log.Default().Println(string(m))
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package websocket
|
package connect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -7,22 +7,26 @@ import (
|
|||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BaseMessage is the base message type for all messages sent by the websocket server.
|
||||||
type BaseMessage struct {
|
type BaseMessage struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"` // not present in all messages
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChanMsg struct {
|
type ChannelMessage struct {
|
||||||
Id int64
|
Id int64
|
||||||
Type string
|
Type string
|
||||||
Success bool
|
Success bool
|
||||||
Raw []byte
|
Raw []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListenWebsocket(conn *websocket.Conn, c chan ChanMsg) {
|
// 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 {
|
for {
|
||||||
bytes, err := ReadMessage(conn)
|
raw, err := ReadMessageRaw(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Error reading from websocket", "err", err)
|
slog.Error("Error reading from websocket", "err", err)
|
||||||
close(c)
|
close(c)
|
||||||
@@ -33,20 +37,26 @@ func ListenWebsocket(conn *websocket.Conn, c chan ChanMsg) {
|
|||||||
// default to true for messages that don't include "success" at all
|
// default to true for messages that don't include "success" at all
|
||||||
Success: true,
|
Success: true,
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(bytes, &base)
|
err = json.Unmarshal(raw, &base)
|
||||||
if !base.Success {
|
if err != nil {
|
||||||
slog.Warn("Received unsuccessful response", "response", string(bytes))
|
slog.Error("Error unmarshalling message", "err", err, "message", string(raw))
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
chanMsg := ChanMsg{
|
if !base.Success {
|
||||||
|
slog.Warn("Received unsuccessful response", "response", string(raw))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a channel message from the raw message
|
||||||
|
channelMessage := ChannelMessage{
|
||||||
Type: base.Type,
|
Type: base.Type,
|
||||||
Id: base.Id,
|
Id: base.Id,
|
||||||
Success: base.Success,
|
Success: base.Success,
|
||||||
Raw: bytes,
|
Raw: raw,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use non-blocking send to avoid hanging on closed channel
|
// Use non-blocking send to avoid hanging on closed channel
|
||||||
select {
|
select {
|
||||||
case c <- chanMsg:
|
case c <- channelMessage:
|
||||||
// Message sent successfully
|
// Message sent successfully
|
||||||
default:
|
default:
|
||||||
// Channel is full or closed, break out of loop
|
// Channel is full or closed, break out of loop
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
@@ -12,40 +13,44 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type HttpClient struct {
|
type HttpClient struct {
|
||||||
client *resty.Client
|
client *resty.Client
|
||||||
|
baseRequest *resty.Request
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpClient(url *url.URL, token string) *HttpClient {
|
func NewHttpClient(ctx context.Context, baseUrl *url.URL, token string) *HttpClient {
|
||||||
// Shallow copy the URL to avoid modifying the original
|
// Shallow copy the URL to avoid modifying the original
|
||||||
u := *url
|
u := *baseUrl
|
||||||
u.Path = "/api"
|
u.Path = "/api"
|
||||||
if u.Scheme == "ws" {
|
|
||||||
u.Scheme = "http"
|
|
||||||
}
|
|
||||||
if u.Scheme == "wss" {
|
|
||||||
u.Scheme = "https"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create resty client with configuration
|
// Create resty client with configuration
|
||||||
client := resty.New().
|
client := resty.New().
|
||||||
SetBaseURL(u.String()).
|
SetBaseURL(u.String()).
|
||||||
SetHeader("Authorization", "Bearer "+token).
|
SetTimeout(30*time.Second).
|
||||||
SetTimeout(30 * time.Second).
|
|
||||||
SetRetryCount(3).
|
SetRetryCount(3).
|
||||||
SetRetryWaitTime(1 * time.Second).
|
SetRetryWaitTime(1*time.Second).
|
||||||
SetRetryMaxWaitTime(5 * time.Second).
|
SetRetryMaxWaitTime(5*time.Second).
|
||||||
AddRetryConditions(func(r *resty.Response, err error) bool {
|
AddRetryConditions(func(r *resty.Response, err error) bool {
|
||||||
return err != nil || r.StatusCode() >= 500
|
return err != nil || (r.StatusCode() >= 500 && r.StatusCode() != 403)
|
||||||
})
|
}).
|
||||||
|
SetHeader("User-Agent", "go-ha/"+currentVersion).
|
||||||
|
SetContext(ctx)
|
||||||
|
|
||||||
return &HttpClient{
|
return &HttpClient{
|
||||||
client: client,
|
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) {
|
func (c *HttpClient) GetState(entityId string) ([]byte, error) {
|
||||||
resp, err := c.client.R().
|
resp, err := c.getRequest().Get("/states/" + entityId)
|
||||||
Get("/states/" + entityId)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Error making HTTP request: " + err.Error())
|
return nil, errors.New("Error making HTTP request: " + err.Error())
|
||||||
@@ -58,9 +63,9 @@ func (c *HttpClient) GetState(entityId string) ([]byte, error) {
|
|||||||
return resp.Bytes(), nil
|
return resp.Bytes(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HttpClient) States() ([]byte, error) {
|
// GetStates returns the states of all entities.
|
||||||
resp, err := c.client.R().
|
func (c *HttpClient) GetStates() ([]byte, error) {
|
||||||
Get("/states")
|
resp, err := c.getRequest().Get("/states")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("Error making HTTP request: " + err.Error())
|
return nil, errors.New("Error making HTTP request: " + err.Error())
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package internal
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -12,6 +13,10 @@ type EnabledDisabledInfo struct {
|
|||||||
RunOnError bool
|
RunOnError bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
currentVersion = "0.7.1"
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
id atomic.Int64 // default value is 0
|
id atomic.Int64 // default value is 0
|
||||||
)
|
)
|
||||||
@@ -26,3 +31,20 @@ func NextId() int64 {
|
|||||||
func GetFunctionName(i interface{}) string {
|
func GetFunctionName(i interface{}) string {
|
||||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
package parse
|
package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-module/carbon"
|
"github.com/dromara/carbon/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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)
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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("")
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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.
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
"github.com/Xevion/go-ha/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"
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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)
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
ws "github.com/Xevion/go-ha/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,8 +17,6 @@ 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 {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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"
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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"
|
||||||
|
|||||||
@@ -4,17 +4,13 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
ws "github.com/Xevion/go-ha/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"
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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"
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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"
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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 {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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 {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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 {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
"github.com/Xevion/go-ha/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.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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 {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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("")
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package services
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
ws "github.com/Xevion/go-ha/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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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"
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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)
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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("")
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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 {
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package services
|
package services
|
||||||
|
|
||||||
import (
|
import (
|
||||||
ws "github.com/Xevion/go-ha/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)
|
||||||
|
|||||||
39
interval.go
39
interval.go
@@ -6,7 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
"github.com/Xevion/go-ha/internal/parse"
|
"github.com/Xevion/go-ha/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IntervalCallback func(*Service, State)
|
type IntervalCallback func(*Service, State)
|
||||||
@@ -14,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
|
||||||
@@ -58,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 ""
|
||||||
}
|
}
|
||||||
@@ -80,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 := parse.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
|
||||||
}
|
}
|
||||||
@@ -104,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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,22 +188,22 @@ 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)
|
||||||
|
|||||||
26
schedule.go
26
schedule.go
@@ -6,8 +6,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Xevion/go-ha/internal"
|
"github.com/Xevion/go-ha/internal"
|
||||||
"github.com/Xevion/go-ha/internal/parse"
|
"github.com/Xevion/go-ha/types"
|
||||||
"github.com/golang-module/carbon"
|
"github.com/dromara/carbon/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ScheduleCallback func(*Service, State)
|
type ScheduleCallback func(*Service, State)
|
||||||
@@ -23,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
|
||||||
@@ -76,7 +76,7 @@ func (sb scheduleBuilder) Call(callback ScheduleCallback) scheduleBuilderCall {
|
|||||||
|
|
||||||
// At takes a string in 24hr format time like "15:30".
|
// At takes a string in 24hr format time like "15:30".
|
||||||
func (sb scheduleBuilderCall) At(s string) scheduleBuilderEnd {
|
func (sb scheduleBuilderCall) At(s string) scheduleBuilderEnd {
|
||||||
t := parse.ParseTime(s)
|
t := internal.ParseTime(s)
|
||||||
sb.schedule.hour = t.Hour()
|
sb.schedule.hour = t.Hour()
|
||||||
sb.schedule.minute = t.Minute()
|
sb.schedule.minute = t.Minute()
|
||||||
return scheduleBuilderEnd(sb)
|
return scheduleBuilderEnd(sb)
|
||||||
@@ -85,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]
|
||||||
@@ -96,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]
|
||||||
@@ -193,16 +193,16 @@ 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)
|
||||||
@@ -215,7 +215,7 @@ func popSchedule(a *App) 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)
|
||||||
@@ -223,9 +223,9 @@ 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.Put(Item{
|
a.schedules.Put(Item{
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package gomeassistant
|
package gomeassistant
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/Xevion/go-ha/internal/connect"
|
||||||
"github.com/Xevion/go-ha/internal/services"
|
"github.com/Xevion/go-ha/internal/services"
|
||||||
ws "github.com/Xevion/go-ha/internal/websocket"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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),
|
||||||
|
|||||||
23
state.go
23
state.go
@@ -6,16 +6,17 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/golang-module/carbon"
|
"github.com/dromara/carbon/v2"
|
||||||
|
|
||||||
internal "github.com/Xevion/go-ha/internal"
|
"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)
|
||||||
@@ -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
35
types/app.go
Normal 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
22
types/common.go
Normal 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
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package gomeassistant
|
package types
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
Reference in New Issue
Block a user