From 5ad7b061fe8be9ef3d27eab156f5c14b793a7abf Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 23 Nov 2025 09:56:28 +0100 Subject: [PATCH 1/7] Be consistent in the naming of variables of type `*App` Previously, some were named `a` and some were named `app`. Name them all `app` for consistency. --- app.go | 88 ++++++++++++++++++++++++++--------------------------- interval.go | 12 ++++---- schedule.go | 20 ++++++------ 3 files changed, 60 insertions(+), 60 deletions(-) diff --git a/app.go b/app.go index 150ce60..03fd1bf 100644 --- a/app.go +++ b/app.go @@ -184,19 +184,19 @@ func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { }, nil } -func (a *App) Cleanup() { - if a.ctxCancel != nil { - a.ctxCancel() +func (app *App) Cleanup() { + if app.ctxCancel != nil { + app.ctxCancel() } } -func (a *App) RegisterSchedules(schedules ...DailySchedule) { +func (app *App) RegisterSchedules(schedules ...DailySchedule) { for _, s := range schedules { // realStartTime already set for sunset/sunrise if s.isSunrise || s.isSunset { - s.nextRunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset).Carbon2Time() - a.scheduledActions = append(a.scheduledActions, s) - a.scheduleCount++ + s.nextRunTime = getNextSunRiseOrSet(app, s.isSunrise, s.sunOffset).Carbon2Time() + app.scheduledActions = append(app.scheduledActions, s) + app.scheduleCount++ continue } @@ -209,12 +209,12 @@ func (a *App) RegisterSchedules(schedules ...DailySchedule) { } s.nextRunTime = startTime.Carbon2Time() - a.scheduledActions = append(a.scheduledActions, s) - a.scheduleCount++ + app.scheduledActions = append(app.scheduledActions, s) + app.scheduleCount++ } } -func (a *App) RegisterIntervals(intervals ...Interval) { +func (app *App) RegisterIntervals(intervals ...Interval) { for _, i := range intervals { if i.frequency == 0 { slog.Error("A schedule must use either set frequency via Every()") @@ -226,11 +226,11 @@ func (a *App) RegisterIntervals(intervals ...Interval) { for i.nextRunTime.Before(now) { i.nextRunTime = i.nextRunTime.Add(i.frequency) } - a.scheduledActions = append(a.scheduledActions, i) + app.scheduledActions = append(app.scheduledActions, i) } } -func (a *App) RegisterEntityListeners(etls ...EntityListener) { +func (app *App) RegisterEntityListeners(etls ...EntityListener) { for _, etl := range etls { etl := etl if etl.delay != 0 && etl.toState == "" { @@ -239,24 +239,24 @@ func (a *App) RegisterEntityListeners(etls ...EntityListener) { } for _, entity := range etl.entityIds { - if elList, ok := a.entityListeners[entity]; ok { - a.entityListeners[entity] = append(elList, &etl) + if elList, ok := app.entityListeners[entity]; ok { + app.entityListeners[entity] = append(elList, &etl) } 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 { evl := evl for _, eventType := range evl.eventTypes { - if elList, ok := a.eventListeners[eventType]; ok { - a.eventListeners[eventType] = append(elList, &evl) + if elList, ok := app.eventListeners[eventType]; ok { + app.eventListeners[eventType] = append(elList, &evl) } else { - websocket.SubscribeToEventType(a.ctx, eventType, a.conn) - a.eventListeners[eventType] = []*EventListener{&evl} + websocket.SubscribeToEventType(app.ctx, eventType, app.conn) + app.eventListeners[eventType] = []*EventListener{&evl} } } } @@ -295,41 +295,41 @@ func getSunriseSunset(s *StateImpl, sunrise bool, dateToUse carbon.Carbon, offse return setOrRiseToday } -func getNextSunRiseOrSet(a *App, sunrise bool, offset ...DurationString) carbon.Carbon { - sunriseOrSunset := getSunriseSunset(a.state, sunrise, carbon.Now(), offset...) +func getNextSunRiseOrSet(app *App, sunrise bool, offset ...DurationString) carbon.Carbon { + sunriseOrSunset := getSunriseSunset(app.state, sunrise, carbon.Now(), offset...) if sunriseOrSunset.Lt(carbon.Now()) { // if we're past today's sunset or sunrise (accounting for offset) then get tomorrows // as that's the next time the schedule will run - sunriseOrSunset = getSunriseSunset(a.state, sunrise, carbon.Tomorrow(), offset...) + sunriseOrSunset = getSunriseSunset(app.state, sunrise, carbon.Tomorrow(), offset...) } return sunriseOrSunset } -func (a *App) Start() { - slog.Info("Starting", "schedules", a.scheduleCount) - slog.Info("Starting", "entity listeners", len(a.entityListeners)) - slog.Info("Starting", "event listeners", len(a.eventListeners)) +func (app *App) Start() { + slog.Info("Starting", "schedules", app.scheduleCount) + slog.Info("Starting", "entity listeners", len(app.entityListeners)) + slog.Info("Starting", "event listeners", len(app.eventListeners)) - go a.runScheduledActions(a.ctx) + go app.runScheduledActions(app.ctx) // subscribe to state_changed events id := internal.GetId() - websocket.SubscribeToStateChangedEvents(a.ctx, id, a.conn) - a.entityListenersId = id + websocket.SubscribeToStateChangedEvents(app.ctx, id, app.conn) + app.entityListenersId = id // entity listeners runOnStartup - for eid, etls := range a.entityListeners { + for eid, etls := range app.entityListeners { for _, etl := range etls { // ensure each ETL only runs once, even if // it listens to multiple entities if etl.runOnStartup && !etl.runOnStartupCompleted { - entityState, err := a.state.Get(eid) + entityState, err := app.state.Get(eid) if err != nil { slog.Warn("Failed to get entity state \"", eid, "\" during startup, skipping RunOnStartup") } etl.runOnStartupCompleted = true - go etl.callback(a.service, a.state, EntityData{ + go etl.callback(app.service, app.state, EntityData{ TriggerEntityId: eid, FromState: entityState.State, FromAttributes: entityState.Attributes, @@ -343,17 +343,17 @@ func (a *App) Start() { // entity listeners and event listeners elChan := make(chan websocket.ChanMsg) - go a.conn.ListenWebsocket(elChan) + go app.conn.ListenWebsocket(elChan) for { msg, ok := <-elChan if !ok { break } - if a.entityListenersId == msg.Id { - go callEntityListeners(a, msg.Raw) + if app.entityListenersId == msg.Id { + go callEntityListeners(app, msg.Raw) } else { - go callEventListeners(a, msg) + go callEventListeners(app, msg) } } } @@ -362,23 +362,23 @@ func (a *App) Start() { // and each `Interval` that has been configured. The `run()` method of // each of those instances takes care of deciding when to run and // invoking its callback. -func (a *App) runScheduledActions(ctx context.Context) { +func (app *App) runScheduledActions(ctx context.Context) { var wg sync.WaitGroup defer wg.Wait() - for _, action := range a.scheduledActions { + for _, action := range app.scheduledActions { wg.Add(1) go func(action scheduledAction) { defer wg.Done() - action.run(ctx, a) + action.run(ctx, app) }(action) } } -func (a *App) GetService() *Service { - return a.service +func (app *App) GetService() *Service { + return app.service } -func (a *App) GetState() State { - return a.state +func (app *App) GetState() State { + return app.state } diff --git a/interval.go b/interval.go index 5458966..2d31949 100644 --- a/interval.go +++ b/interval.go @@ -149,7 +149,7 @@ func (sb intervalBuilderEnd) Build() Interval { // run invokes `i.maybeRunCallback()` based on its configured // frequency. -func (i Interval) run(ctx context.Context, a *App) { +func (i Interval) run(ctx context.Context, app *App) { // Create a new, but stopped, timer for sleeping on: timer := time.NewTimer(1 * time.Hour) if !timer.Stop() { @@ -167,12 +167,12 @@ func (i Interval) run(ctx context.Context, a *App) { } } - i.maybeRunCallback(a) + i.maybeRunCallback(app) i.nextRunTime = i.nextRunTime.Add(i.frequency) } } -func (i Interval) maybeRunCallback(a *App) { +func (i Interval) maybeRunCallback(app *App) { if c := checkStartEndTime(i.startTime /* isStart = */, true); c.fail { return } @@ -185,11 +185,11 @@ func (i Interval) maybeRunCallback(a *App) { if c := checkExceptionRanges(i.exceptionRanges); c.fail { return } - if c := checkEnabledEntity(a.state, i.enabledEntities); c.fail { + if c := checkEnabledEntity(app.state, i.enabledEntities); c.fail { return } - if c := checkDisabledEntity(a.state, i.disabledEntities); c.fail { + if c := checkDisabledEntity(app.state, i.disabledEntities); c.fail { return } - go i.callback(a.service, a.state) + go i.callback(app.service, app.state) } diff --git a/schedule.go b/schedule.go index 979c2f4..2113893 100644 --- a/schedule.go +++ b/schedule.go @@ -155,7 +155,7 @@ func (sb scheduleBuilderEnd) Build() DailySchedule { // run invokes `s.maybeRunCallback()` based on its configured // schedule. Terminate when `ctx` is canceled. -func (s DailySchedule) run(ctx context.Context, a *App) { +func (s DailySchedule) run(ctx context.Context, app *App) { // Create a new, but stopped, timer for sleeping on: timer := time.NewTimer(1 * time.Hour) if !timer.Stop() { @@ -175,37 +175,37 @@ func (s DailySchedule) run(ctx context.Context, a *App) { } } - s.maybeRunCallback(a) - s.updateNextRunTime(a) + s.maybeRunCallback(app) + s.updateNextRunTime(app) } } -func (s DailySchedule) maybeRunCallback(a *App) { +func (s DailySchedule) maybeRunCallback(app *App) { if c := checkExceptionDates(s.exceptionDates); c.fail { return } if c := checkAllowlistDates(s.allowlistDates); c.fail { return } - if c := checkEnabledEntity(a.state, s.enabledEntities); c.fail { + if c := checkEnabledEntity(app.state, s.enabledEntities); c.fail { return } - if c := checkDisabledEntity(a.state, s.disabledEntities); c.fail { + if c := checkDisabledEntity(app.state, s.disabledEntities); c.fail { return } - go s.callback(a.service, a.state) + go s.callback(app.service, app.state) } // updateNextRunTime updates `s.nextRunTime` to the next time that `s` // should run. -func (s DailySchedule) updateNextRunTime(a *App) { +func (s DailySchedule) updateNextRunTime(app *App) { if s.isSunrise || s.isSunset { var nextSunTime carbon.Carbon // "0s" is default value if s.sunOffset != "0s" { - nextSunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset) + nextSunTime = getNextSunRiseOrSet(app, s.isSunrise, s.sunOffset) } else { - nextSunTime = getNextSunRiseOrSet(a, s.isSunrise) + nextSunTime = getNextSunRiseOrSet(app, s.isSunrise) } s.nextRunTime = nextSunTime.Carbon2Time() From 364a51c9eb4c0e0c6f9e5e25bdb82e84eea31aaa Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 5 Dec 2025 15:36:18 +0100 Subject: [PATCH 2/7] Remove the `ctx` parameter from the subscribe functions The functions `SubscribeToEventType()` and `SubscribeToStateChangedEvents()` didn't use their `ctx` parameters, and they don't really make sense. So remove them. --- app.go | 4 ++-- internal/websocket/websocket.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 03fd1bf..a9bd1a3 100644 --- a/app.go +++ b/app.go @@ -255,7 +255,7 @@ func (app *App) RegisterEventListeners(evls ...EventListener) { if elList, ok := app.eventListeners[eventType]; ok { app.eventListeners[eventType] = append(elList, &evl) } else { - websocket.SubscribeToEventType(app.ctx, eventType, app.conn) + websocket.SubscribeToEventType(eventType, app.conn) app.eventListeners[eventType] = []*EventListener{&evl} } } @@ -314,7 +314,7 @@ func (app *App) Start() { // subscribe to state_changed events id := internal.GetId() - websocket.SubscribeToStateChangedEvents(app.ctx, id, app.conn) + websocket.SubscribeToStateChangedEvents(id, app.conn) app.entityListenersId = id // entity listeners runOnStartup diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 3b1376e..55fd284 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -135,11 +135,11 @@ type SubEvent struct { EventType string `json:"event_type"` } -func SubscribeToStateChangedEvents(ctx context.Context, id int64, conn *Conn) { - SubscribeToEventType(ctx, "state_changed", conn, id) +func SubscribeToStateChangedEvents(id int64, conn *Conn) { + SubscribeToEventType("state_changed", conn, id) } -func SubscribeToEventType(ctx context.Context, eventType string, conn *Conn, id ...int64) { +func SubscribeToEventType(eventType string, conn *Conn, id ...int64) { var finalId int64 if len(id) == 0 { finalId = internal.GetId() From f093ade2f2795013f173f1165b0c97e84c9bbaa7 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 5 Dec 2025 16:58:29 +0100 Subject: [PATCH 3/7] Add a type representing how services interact with the HA API For now, it only includes a single method, `WriteMessage()`, provided by `*Conn`. But that will change soon. --- internal/services/adaptive_lighting.go | 8 +--- internal/services/alarm_control_panel.go | 20 ++++------ internal/services/climate.go | 7 ++-- internal/services/cover.go | 26 ++++++------ internal/services/event.go | 5 +-- internal/services/homeassistant.go | 12 ++---- internal/services/input_boolean.go | 14 +++---- internal/services/input_button.go | 10 ++--- internal/services/input_datetime.go | 8 ++-- internal/services/input_number.go | 14 +++---- internal/services/input_text.go | 10 ++--- internal/services/light.go | 12 ++---- internal/services/lock.go | 10 ++--- internal/services/media_player.go | 50 +++++++++++------------- internal/services/notify.go | 5 +-- internal/services/number.go | 8 +--- internal/services/scene.go | 14 +++---- internal/services/script.go | 14 +++---- internal/services/services.go | 11 ++++-- internal/services/switch.go | 12 ++---- internal/services/timer.go | 18 ++++----- internal/services/tts.go | 12 ++---- internal/services/vacuum.go | 28 ++++++------- internal/services/zwavejs.go | 8 +--- 24 files changed, 130 insertions(+), 206 deletions(-) diff --git a/internal/services/adaptive_lighting.go b/internal/services/adaptive_lighting.go index abfb29f..fde345f 100644 --- a/internal/services/adaptive_lighting.go +++ b/internal/services/adaptive_lighting.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type AdaptiveLighting struct { - conn *websocket.Conn + api API } /* Public API */ @@ -22,5 +18,5 @@ func (al AdaptiveLighting) SetManualControl(entityId string, enabled bool) error "manual_control": enabled, } - return al.conn.WriteMessage(req) + return al.api.WriteMessage(req) } diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 3b8858e..86b6403 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type AlarmControlPanel struct { - conn *websocket.Conn + api API } /* Public API */ @@ -23,7 +19,7 @@ func (acp AlarmControlPanel) ArmAway(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - return acp.conn.WriteMessage(req) + return acp.api.WriteMessage(req) } // Send the alarm the command for arm away. @@ -37,7 +33,7 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData .. req.ServiceData = serviceData[0] } - return acp.conn.WriteMessage(req) + return acp.api.WriteMessage(req) } // Send the alarm the command for arm home. @@ -51,7 +47,7 @@ func (acp AlarmControlPanel) ArmHome(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - return acp.conn.WriteMessage(req) + return acp.api.WriteMessage(req) } // Send the alarm the command for arm night. @@ -65,7 +61,7 @@ func (acp AlarmControlPanel) ArmNight(entityId string, serviceData ...map[string req.ServiceData = serviceData[0] } - return acp.conn.WriteMessage(req) + return acp.api.WriteMessage(req) } // Send the alarm the command for arm vacation. @@ -79,7 +75,7 @@ func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData ...map[str req.ServiceData = serviceData[0] } - return acp.conn.WriteMessage(req) + return acp.api.WriteMessage(req) } // Send the alarm the command for disarm. @@ -93,7 +89,7 @@ func (acp AlarmControlPanel) Disarm(entityId string, serviceData ...map[string]a req.ServiceData = serviceData[0] } - return acp.conn.WriteMessage(req) + return acp.api.WriteMessage(req) } // Send the alarm the command for trigger. @@ -107,5 +103,5 @@ func (acp AlarmControlPanel) Trigger(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - return acp.conn.WriteMessage(req) + return acp.api.WriteMessage(req) } diff --git a/internal/services/climate.go b/internal/services/climate.go index 74d167d..46353e2 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -1,14 +1,13 @@ package services import ( - "saml.dev/gome-assistant/internal/websocket" "saml.dev/gome-assistant/types" ) /* Structs */ type Climate struct { - conn *websocket.Conn + api API } /* Public API */ @@ -19,7 +18,7 @@ func (c Climate) SetFanMode(entityId string, fanMode string) error { req.Service = "set_fan_mode" req.ServiceData = map[string]any{"fan_mode": fanMode} - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatureRequest) error { @@ -28,5 +27,5 @@ func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatur req.Service = "set_temperature" req.ServiceData = serviceData.ToJSON() - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } diff --git a/internal/services/cover.go b/internal/services/cover.go index 4a71893..6b91220 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Cover struct { - conn *websocket.Conn + api API } /* Public API */ @@ -18,7 +14,7 @@ func (c Cover) Close(entityId string) error { req.Domain = "cover" req.Service = "close_cover" - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Close all or specified cover tilt. Takes an entityId. @@ -27,7 +23,7 @@ func (c Cover) CloseTilt(entityId string) error { req.Domain = "cover" req.Service = "close_cover_tilt" - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Open all or specified cover. Takes an entityId. @@ -36,7 +32,7 @@ func (c Cover) Open(entityId string) error { req.Domain = "cover" req.Service = "open_cover" - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Open all or specified cover tilt. Takes an entityId. @@ -45,7 +41,7 @@ func (c Cover) OpenTilt(entityId string) error { req.Domain = "cover" req.Service = "open_cover_tilt" - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Move to specific position all or specified cover. Takes an entityId and an optional @@ -58,7 +54,7 @@ func (c Cover) SetPosition(entityId string, serviceData ...map[string]any) error req.ServiceData = serviceData[0] } - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Move to specific position all or specified cover tilt. Takes an entityId and an optional @@ -71,7 +67,7 @@ func (c Cover) SetTiltPosition(entityId string, serviceData ...map[string]any) e req.ServiceData = serviceData[0] } - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Stop a cover entity. Takes an entityId. @@ -80,7 +76,7 @@ func (c Cover) Stop(entityId string) error { req.Domain = "cover" req.Service = "stop_cover" - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Stop a cover entity tilt. Takes an entityId. @@ -89,7 +85,7 @@ func (c Cover) StopTilt(entityId string) error { req.Domain = "cover" req.Service = "stop_cover_tilt" - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Toggle a cover open/closed. Takes an entityId. @@ -98,7 +94,7 @@ func (c Cover) Toggle(entityId string) error { req.Domain = "cover" req.Service = "toggle" - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } // Toggle a cover tilt open/closed. Takes an entityId. @@ -107,5 +103,5 @@ func (c Cover) ToggleTilt(entityId string) error { req.Domain = "cover" req.Service = "toggle_cover_tilt" - return c.conn.WriteMessage(req) + return c.api.WriteMessage(req) } diff --git a/internal/services/event.go b/internal/services/event.go index 67ff39d..c5b5ac1 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -2,11 +2,10 @@ package services import ( "saml.dev/gome-assistant/internal" - "saml.dev/gome-assistant/internal/websocket" ) type Event struct { - conn *websocket.Conn + api API } // Fire an event @@ -32,5 +31,5 @@ func (e Event) Fire(eventType string, eventData ...map[string]any) error { req.EventData = eventData[0] } - return e.conn.WriteMessage(req) + return e.api.WriteMessage(req) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index d888f85..601e9d4 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -1,11 +1,7 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - type HomeAssistant struct { - conn *websocket.Conn + api API } // TurnOn a Home Assistant entity. Takes an entityId and an optional @@ -18,7 +14,7 @@ func (ha *HomeAssistant) TurnOn(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - return ha.conn.WriteMessage(req) + return ha.api.WriteMessage(req) } // Toggle a Home Assistant entity. Takes an entityId and an optional @@ -31,7 +27,7 @@ func (ha *HomeAssistant) Toggle(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - return ha.conn.WriteMessage(req) + return ha.api.WriteMessage(req) } func (ha *HomeAssistant) TurnOff(entityId string) error { @@ -39,5 +35,5 @@ func (ha *HomeAssistant) TurnOff(entityId string) error { req.Domain = "homeassistant" req.Service = "turn_off" - return ha.conn.WriteMessage(req) + return ha.api.WriteMessage(req) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 788b830..c7df9c7 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type InputBoolean struct { - conn *websocket.Conn + api API } /* Public API */ @@ -17,7 +13,7 @@ func (ib InputBoolean) TurnOn(entityId string) error { req.Domain = "input_boolean" req.Service = "turn_on" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputBoolean) Toggle(entityId string) error { @@ -25,19 +21,19 @@ func (ib InputBoolean) Toggle(entityId string) error { req.Domain = "input_boolean" req.Service = "toggle" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputBoolean) TurnOff(entityId string) error { req := NewBaseServiceRequest(entityId) req.Domain = "input_boolean" req.Service = "turn_off" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputBoolean) Reload() error { req := NewBaseServiceRequest("") req.Domain = "input_boolean" req.Service = "reload" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 7a20657..58d7496 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type InputButton struct { - conn *websocket.Conn + api API } /* Public API */ @@ -17,12 +13,12 @@ func (ib InputButton) Press(entityId string) error { req.Domain = "input_button" req.Service = "press" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputButton) Reload() error { req := NewBaseServiceRequest("") req.Domain = "input_button" req.Service = "reload" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 0306838..5a17b3f 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -3,14 +3,12 @@ package services import ( "fmt" "time" - - "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputDatetime struct { - conn *websocket.Conn + api API } /* Public API */ @@ -23,12 +21,12 @@ func (ib InputDatetime) Set(entityId string, value time.Time) error { "timestamp": fmt.Sprint(value.Unix()), } - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputDatetime) Reload() error { req := NewBaseServiceRequest("") req.Domain = "input_datetime" req.Service = "reload" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 596285f..9c82796 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type InputNumber struct { - conn *websocket.Conn + api API } /* Public API */ @@ -18,7 +14,7 @@ func (ib InputNumber) Set(entityId string, value float32) error { req.Service = "set_value" req.ServiceData = map[string]any{"value": value} - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputNumber) Increment(entityId string) error { @@ -26,7 +22,7 @@ func (ib InputNumber) Increment(entityId string) error { req.Domain = "input_number" req.Service = "increment" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputNumber) Decrement(entityId string) error { @@ -34,12 +30,12 @@ func (ib InputNumber) Decrement(entityId string) error { req.Domain = "input_number" req.Service = "decrement" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputNumber) Reload() error { req := NewBaseServiceRequest("") req.Domain = "input_number" req.Service = "reload" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index ed6789e..1486b00 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type InputText struct { - conn *websocket.Conn + api API } /* Public API */ @@ -20,12 +16,12 @@ func (ib InputText) Set(entityId string, value string) error { "value": value, } - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib InputText) Reload() error { req := NewBaseServiceRequest("") req.Domain = "input_text" req.Service = "reload" - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } diff --git a/internal/services/light.go b/internal/services/light.go index dedb9b1..013c69c 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Light struct { - conn *websocket.Conn + api API } /* Public API */ @@ -22,7 +18,7 @@ func (l Light) TurnOn(entityId string, serviceData ...map[string]any) error { req.ServiceData = serviceData[0] } - return l.conn.WriteMessage(req) + return l.api.WriteMessage(req) } // Toggle a light entity. Takes an entityId and an optional @@ -35,12 +31,12 @@ func (l Light) Toggle(entityId string, serviceData ...map[string]any) error { req.ServiceData = serviceData[0] } - return l.conn.WriteMessage(req) + return l.api.WriteMessage(req) } func (l Light) TurnOff(entityId string) error { req := NewBaseServiceRequest(entityId) req.Domain = "light" req.Service = "turn_off" - return l.conn.WriteMessage(req) + return l.api.WriteMessage(req) } diff --git a/internal/services/lock.go b/internal/services/lock.go index f6e831a..2246765 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Lock struct { - conn *websocket.Conn + api API } /* Public API */ @@ -22,7 +18,7 @@ func (l Lock) Lock(entityId string, serviceData ...map[string]any) error { req.ServiceData = serviceData[0] } - return l.conn.WriteMessage(req) + return l.api.WriteMessage(req) } // Unlock a lock entity. Takes an entityId and an optional @@ -35,5 +31,5 @@ func (l Lock) Unlock(entityId string, serviceData ...map[string]any) error { req.ServiceData = serviceData[0] } - return l.conn.WriteMessage(req) + return l.api.WriteMessage(req) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index bc64499..7a43066 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type MediaPlayer struct { - conn *websocket.Conn + api API } /* Public API */ @@ -19,7 +15,7 @@ func (mp MediaPlayer) ClearPlaylist(entityId string) error { req.Domain = "media_player" req.Service = "clear_playlist" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Group players together. Only works on platforms with support for player groups. @@ -33,7 +29,7 @@ func (mp MediaPlayer) Join(entityId string, serviceData ...map[string]any) error req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the command for next track. @@ -43,7 +39,7 @@ func (mp MediaPlayer) Next(entityId string) error { req.Domain = "media_player" req.Service = "media_next_track" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the command for pause. @@ -53,7 +49,7 @@ func (mp MediaPlayer) Pause(entityId string) error { req.Domain = "media_player" req.Service = "media_pause" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the command for play. @@ -63,7 +59,7 @@ func (mp MediaPlayer) Play(entityId string) error { req.Domain = "media_player" req.Service = "media_play" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Toggle media player play/pause state. @@ -73,7 +69,7 @@ func (mp MediaPlayer) PlayPause(entityId string) error { req.Domain = "media_player" req.Service = "media_play_pause" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the command for previous track. @@ -83,7 +79,7 @@ func (mp MediaPlayer) Previous(entityId string) error { req.Domain = "media_player" req.Service = "media_previous_track" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the command to seek in current playing media. @@ -97,7 +93,7 @@ func (mp MediaPlayer) Seek(entityId string, serviceData ...map[string]any) error req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the stop command. @@ -107,7 +103,7 @@ func (mp MediaPlayer) Stop(entityId string) error { req.Domain = "media_player" req.Service = "media_stop" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the command for playing media. @@ -121,7 +117,7 @@ func (mp MediaPlayer) PlayMedia(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Set repeat mode. Takes an entityId and an optional @@ -134,7 +130,7 @@ func (mp MediaPlayer) RepeatSet(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the command to change sound mode. @@ -148,7 +144,7 @@ func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData ...map[string req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Send the media player the command to change input source. @@ -162,7 +158,7 @@ func (mp MediaPlayer) SelectSource(entityId string, serviceData ...map[string]an req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Set shuffling state. @@ -176,7 +172,7 @@ func (mp MediaPlayer) Shuffle(entityId string, serviceData ...map[string]any) er req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Toggles a media player power state. @@ -186,7 +182,7 @@ func (mp MediaPlayer) Toggle(entityId string) error { req.Domain = "media_player" req.Service = "toggle" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Turn a media player power off. @@ -196,7 +192,7 @@ func (mp MediaPlayer) TurnOff(entityId string) error { req.Domain = "media_player" req.Service = "turn_off" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Turn a media player power on. @@ -206,7 +202,7 @@ func (mp MediaPlayer) TurnOn(entityId string) error { req.Domain = "media_player" req.Service = "turn_on" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Unjoin the player from a group. Only works on @@ -217,7 +213,7 @@ func (mp MediaPlayer) Unjoin(entityId string) error { req.Domain = "media_player" req.Service = "unjoin" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Turn a media player volume down. @@ -227,7 +223,7 @@ func (mp MediaPlayer) VolumeDown(entityId string) error { req.Domain = "media_player" req.Service = "volume_down" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Mute a media player's volume. @@ -241,7 +237,7 @@ func (mp MediaPlayer) VolumeMute(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Set a media player's volume level. @@ -255,7 +251,7 @@ func (mp MediaPlayer) VolumeSet(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } // Turn a media player volume up. @@ -265,5 +261,5 @@ func (mp MediaPlayer) VolumeUp(entityId string) error { req.Domain = "media_player" req.Service = "volume_up" - return mp.conn.WriteMessage(req) + return mp.api.WriteMessage(req) } diff --git a/internal/services/notify.go b/internal/services/notify.go index 22047fc..a74d82d 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -1,12 +1,11 @@ package services import ( - "saml.dev/gome-assistant/internal/websocket" "saml.dev/gome-assistant/types" ) type Notify struct { - conn *websocket.Conn + api API } // Notify sends a notification. Takes a types.NotifyRequest. @@ -23,5 +22,5 @@ func (ha *Notify) Notify(reqData types.NotifyRequest) error { } req.ServiceData = serviceData - return ha.conn.WriteMessage(req) + return ha.api.WriteMessage(req) } diff --git a/internal/services/number.go b/internal/services/number.go index 8a215b2..75d450b 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -1,11 +1,7 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - type Number struct { - conn *websocket.Conn + api API } func (ib Number) SetValue(entityId string, value float32) error { @@ -14,7 +10,7 @@ func (ib Number) SetValue(entityId string, value float32) error { req.Service = "set_value" req.ServiceData = map[string]any{"value": value} - return ib.conn.WriteMessage(req) + return ib.api.WriteMessage(req) } func (ib Number) MustSetValue(entityId string, value float32) { diff --git a/internal/services/scene.go b/internal/services/scene.go index ed729a2..4455d04 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Scene struct { - conn *websocket.Conn + api API } /* Public API */ @@ -21,7 +17,7 @@ func (s Scene) Apply(serviceData ...map[string]any) error { req.ServiceData = serviceData[0] } - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } // Create a scene entity. Takes an entityId and an optional @@ -34,7 +30,7 @@ func (s Scene) Create(entityId string, serviceData ...map[string]any) error { req.ServiceData = serviceData[0] } - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } // Reload the scenes. @@ -43,7 +39,7 @@ func (s Scene) Reload() error { req.Domain = "scene" req.Service = "reload" - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } // TurnOn a scene entity. Takes an entityId and an optional @@ -56,5 +52,5 @@ func (s Scene) TurnOn(entityId string, serviceData ...map[string]any) error { req.ServiceData = serviceData[0] } - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } diff --git a/internal/services/script.go b/internal/services/script.go index dbe6d0f..9ec0521 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Script struct { - conn *websocket.Conn + api API } /* Public API */ @@ -18,7 +14,7 @@ func (s Script) Reload(entityId string) error { req.Domain = "script" req.Service = "reload" - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } // Toggle a script that was created in the HA UI. @@ -27,7 +23,7 @@ func (s Script) Toggle(entityId string) error { req.Domain = "script" req.Service = "toggle" - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } // TurnOff a script that was created in the HA UI. @@ -36,7 +32,7 @@ func (s Script) TurnOff() error { req.Domain = "script" req.Service = "turn_off" - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } // TurnOn a script that was created in the HA UI. @@ -45,5 +41,5 @@ func (s Script) TurnOn(entityId string) error { req.Domain = "script" req.Service = "turn_on" - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } diff --git a/internal/services/services.go b/internal/services/services.go index 80bc564..4980dc6 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -2,9 +2,14 @@ package services import ( "saml.dev/gome-assistant/internal" - "saml.dev/gome-assistant/internal/websocket" ) +// API is the interface that the individual services use to interact +// with HomeAssistant. +type API interface { + WriteMessage(msg any) error +} + func BuildService[ T AdaptiveLighting | AlarmControlPanel | @@ -29,8 +34,8 @@ func BuildService[ Timer | Vacuum | ZWaveJS, -](conn *websocket.Conn) *T { - return &T{conn: conn} +](api API) *T { + return &T{api: api} } type BaseServiceRequest struct { diff --git a/internal/services/switch.go b/internal/services/switch.go index dee37ee..49a02bb 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Switch struct { - conn *websocket.Conn + api API } /* Public API */ @@ -17,7 +13,7 @@ func (s Switch) TurnOn(entityId string) error { req.Domain = "switch" req.Service = "turn_on" - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } func (s Switch) Toggle(entityId string) error { @@ -25,7 +21,7 @@ func (s Switch) Toggle(entityId string) error { req.Domain = "switch" req.Service = "toggle" - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } func (s Switch) TurnOff(entityId string) error { @@ -33,5 +29,5 @@ func (s Switch) TurnOff(entityId string) error { req.Domain = "switch" req.Service = "turn_off" - return s.conn.WriteMessage(req) + return s.api.WriteMessage(req) } diff --git a/internal/services/timer.go b/internal/services/timer.go index 0dfc70d..f000c24 100644 --- a/internal/services/timer.go +++ b/internal/services/timer.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Timer struct { - conn *websocket.Conn + api API } /* Public API */ @@ -21,7 +17,7 @@ func (t Timer) Start(entityId string, duration string) error { "duration": duration, } - return t.conn.WriteMessage(req) + return t.api.WriteMessage(req) } // See https://www.home-assistant.io/integrations/timer/#action-timerstart @@ -33,7 +29,7 @@ func (t Timer) Change(entityId string, duration string) error { "duration": duration, } - return t.conn.WriteMessage(req) + return t.api.WriteMessage(req) } // See https://www.home-assistant.io/integrations/timer/#action-timerpause @@ -41,7 +37,7 @@ func (t Timer) Pause(entityId string) error { req := NewBaseServiceRequest(entityId) req.Domain = "timer" req.Service = "pause" - return t.conn.WriteMessage(req) + return t.api.WriteMessage(req) } // See https://www.home-assistant.io/integrations/timer/#action-timercancel @@ -49,7 +45,7 @@ func (t Timer) Cancel() error { req := NewBaseServiceRequest("") req.Domain = "timer" req.Service = "cancel" - return t.conn.WriteMessage(req) + return t.api.WriteMessage(req) } // See https://www.home-assistant.io/integrations/timer/#action-timerfinish @@ -57,7 +53,7 @@ func (t Timer) Finish(entityId string) error { req := NewBaseServiceRequest(entityId) req.Domain = "timer" req.Service = "finish" - return t.conn.WriteMessage(req) + return t.api.WriteMessage(req) } // See https://www.home-assistant.io/integrations/timer/#action-timerreload @@ -65,5 +61,5 @@ func (t Timer) Reload() error { req := NewBaseServiceRequest("") req.Domain = "timer" req.Service = "reload" - return t.conn.WriteMessage(req) + return t.api.WriteMessage(req) } diff --git a/internal/services/tts.go b/internal/services/tts.go index 60fcf85..8dfcc85 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type TTS struct { - conn *websocket.Conn + api API } /* Public API */ @@ -18,7 +14,7 @@ func (tts TTS) ClearCache() error { req.Domain = "tts" req.Service = "clear_cache" - return tts.conn.WriteMessage(req) + return tts.api.WriteMessage(req) } // Say something using text-to-speech on a media player with cloud. @@ -32,7 +28,7 @@ func (tts TTS) CloudSay(entityId string, serviceData ...map[string]any) error { req.ServiceData = serviceData[0] } - return tts.conn.WriteMessage(req) + return tts.api.WriteMessage(req) } // Say something using text-to-speech on a media player with google_translate. @@ -46,5 +42,5 @@ func (tts TTS) GoogleTranslateSay(entityId string, serviceData ...map[string]any req.ServiceData = serviceData[0] } - return tts.conn.WriteMessage(req) + return tts.api.WriteMessage(req) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 1d1c136..07f68ed 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Vacuum struct { - conn *websocket.Conn + api API } /* Public API */ @@ -19,7 +15,7 @@ func (v Vacuum) CleanSpot(entityId string) error { req.Domain = "vacuum" req.Service = "clean_spot" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Locate the vacuum cleaner robot. @@ -29,7 +25,7 @@ func (v Vacuum) Locate(entityId string) error { req.Domain = "vacuum" req.Service = "locate" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Pause the cleaning task. @@ -39,7 +35,7 @@ func (v Vacuum) Pause(entityId string) error { req.Domain = "vacuum" req.Service = "pause" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Tell the vacuum cleaner to return to its dock. @@ -49,7 +45,7 @@ func (v Vacuum) ReturnToBase(entityId string) error { req.Domain = "vacuum" req.Service = "return_to_base" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Send a raw command to the vacuum cleaner. Takes an entityId and an optional @@ -62,7 +58,7 @@ func (v Vacuum) SendCommand(entityId string, serviceData ...map[string]any) erro req.ServiceData = serviceData[0] } - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Set the fan speed of the vacuum cleaner. Takes an entityId and an optional @@ -76,7 +72,7 @@ func (v Vacuum) SetFanSpeed(entityId string, serviceData ...map[string]any) erro req.ServiceData = serviceData[0] } - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Start or resume the cleaning task. @@ -86,7 +82,7 @@ func (v Vacuum) Start(entityId string) error { req.Domain = "vacuum" req.Service = "start" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Start, pause, or resume the cleaning task. @@ -96,7 +92,7 @@ func (v Vacuum) StartPause(entityId string) error { req.Domain = "vacuum" req.Service = "start_pause" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Stop the current cleaning task. @@ -106,7 +102,7 @@ func (v Vacuum) Stop(entityId string) error { req.Domain = "vacuum" req.Service = "stop" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Stop the current cleaning task and return to home. @@ -116,7 +112,7 @@ func (v Vacuum) TurnOff(entityId string) error { req.Domain = "vacuum" req.Service = "turn_off" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } // Start a new cleaning task. @@ -126,5 +122,5 @@ func (v Vacuum) TurnOn(entityId string) error { req.Domain = "vacuum" req.Service = "turn_on" - return v.conn.WriteMessage(req) + return v.api.WriteMessage(req) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 9baf96c..e09cde6 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -1,13 +1,9 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type ZWaveJS struct { - conn *websocket.Conn + api API } /* Public API */ @@ -22,5 +18,5 @@ func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, valu "value": value, } - return zw.conn.WriteMessage(req) + return zw.api.WriteMessage(req) } From 4f84c9d5d5f1846a78e63a930ec38f0a3a2068ca Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 5 Dec 2025 19:51:16 +0100 Subject: [PATCH 4/7] Conn.writeLock: field renamed from `mutex` There's soon going to be another lock, so give this one a more specific name. --- internal/websocket/websocket.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 55fd284..453485a 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -26,13 +26,13 @@ type AuthMessage struct { } type Conn struct { - conn *websocket.Conn - mutex sync.Mutex + conn *websocket.Conn + writeLock sync.Mutex } func (conn *Conn) WriteMessage(msg any) error { - conn.mutex.Lock() - defer conn.mutex.Unlock() + conn.writeLock.Lock() + defer conn.writeLock.Unlock() return conn.conn.WriteJSON(msg) } From 1cec4694abc6e62a131655ead5e4cb45e373552e Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 5 Dec 2025 22:54:39 +0100 Subject: [PATCH 5/7] App.Call(), FireEvent(): new methods Use new `App` methods to call HA services and fire HA events. This is not super interesting yet, but the work done by `Call()` will increase soon, making this helper method more of a win. --- app.go | 21 +- call.go | 12 ++ fire_event.go | 24 +++ internal/services/adaptive_lighting.go | 16 +- internal/services/alarm_control_panel.go | 70 ++++--- internal/services/climate.go | 26 +-- internal/services/cover.go | 108 +++++----- internal/services/event.go | 25 +-- internal/services/homeassistant.go | 31 +-- internal/services/input_boolean.go | 41 ++-- internal/services/input_button.go | 21 +- internal/services/input_datetime.go | 24 +-- internal/services/input_number.go | 44 +++-- internal/services/input_text.go | 24 +-- internal/services/light.go | 32 +-- internal/services/lock.go | 22 ++- internal/services/media_player.go | 242 ++++++++++++----------- internal/services/notify.go | 11 +- internal/services/number.go | 13 +- internal/services/scene.go | 44 +++-- internal/services/script.go | 44 +++-- internal/services/services.go | 29 ++- internal/services/switch.go | 33 ++-- internal/services/timer.go | 70 ++++--- internal/services/tts.go | 32 +-- internal/services/vacuum.go | 120 ++++++----- internal/services/zwavejs.go | 17 +- service.go | 49 +++-- 28 files changed, 687 insertions(+), 558 deletions(-) create mode 100644 call.go create mode 100644 fire_event.go diff --git a/app.go b/app.go index a9bd1a3..48bdbe2 100644 --- a/app.go +++ b/app.go @@ -160,28 +160,31 @@ func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { httpClient := http.NewHttpClient(baseURL, request.HAAuthToken) - service := newService(conn) state, err := newState(httpClient, request.HomeZoneEntityId) if err != nil { return nil, err } - // Validate home zone - if err := validateHomeZone(state, request.HomeZoneEntityId); err != nil { - return nil, err - } - ctx, cancel := context.WithCancel(ctx) - return &App{ + + app := App{ conn: conn, ctx: ctx, ctxCancel: cancel, httpClient: httpClient, - service: service, state: state, entityListeners: map[string][]*EntityListener{}, eventListeners: map[string][]*EventListener{}, - }, nil + } + + app.service = newService(&app) + + // Validate home zone + if err := validateHomeZone(state, request.HomeZoneEntityId); err != nil { + return nil, err + } + + return &app, nil } func (app *App) Cleanup() { diff --git a/call.go b/call.go new file mode 100644 index 0000000..03b8e4b --- /dev/null +++ b/call.go @@ -0,0 +1,12 @@ +package gomeassistant + +import ( + "saml.dev/gome-assistant/internal" + "saml.dev/gome-assistant/internal/services" +) + +func (app *App) Call(req services.BaseServiceRequest) error { + req.RequestType = "call_service" + req.Id = internal.GetId() + return app.conn.WriteMessage(req) +} diff --git a/fire_event.go b/fire_event.go new file mode 100644 index 0000000..913d690 --- /dev/null +++ b/fire_event.go @@ -0,0 +1,24 @@ +package gomeassistant + +import ( + "saml.dev/gome-assistant/internal" +) + +func (app *App) FireEvent(eventType string, eventData map[string]any) error { + req := FireEventRequest{ + Id: internal.GetId(), + Type: "fire_event", + EventType: eventType, + EventData: eventData, + } + + return app.conn.WriteMessage(req) +} + +// Fire an event +type FireEventRequest struct { + Id int64 `json:"id"` + Type string `json:"type"` // always set to "fire_event" + EventType string `json:"event_type"` + EventData map[string]any `json:"event_data,omitempty"` +} diff --git a/internal/services/adaptive_lighting.go b/internal/services/adaptive_lighting.go index fde345f..c09485b 100644 --- a/internal/services/adaptive_lighting.go +++ b/internal/services/adaptive_lighting.go @@ -10,13 +10,15 @@ type AdaptiveLighting struct { // Set manual control for an adaptive lighting entity. func (al AdaptiveLighting) SetManualControl(entityId string, enabled bool) error { - req := NewBaseServiceRequest("") - req.Domain = "adaptive_lighting" - req.Service = "set_manual_control" - req.ServiceData = map[string]any{ - "entity_id": entityId, - "manual_control": enabled, + req := BaseServiceRequest{ + Domain: "adaptive_lighting", + Service: "set_manual_control", + ServiceData: map[string]any{ + "entity_id": entityId, + "manual_control": enabled, + }, + Target: Entity(entityId), } - return al.api.WriteMessage(req) + return al.api.Call(req) } diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 86b6403..fa02e65 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -12,96 +12,110 @@ type AlarmControlPanel struct { // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmAway(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_away" + req := BaseServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_away", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return acp.api.WriteMessage(req) + return acp.api.Call(req) } // Send the alarm the command for arm away. // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_custom_bypass" + req := BaseServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_custom_bypass", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return acp.api.WriteMessage(req) + return acp.api.Call(req) } // Send the alarm the command for arm home. // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmHome(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_home" + req := BaseServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_home", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return acp.api.WriteMessage(req) + return acp.api.Call(req) } // Send the alarm the command for arm night. // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmNight(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_night" + req := BaseServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_night", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return acp.api.WriteMessage(req) + return acp.api.Call(req) } // Send the alarm the command for arm vacation. // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_vacation" + req := BaseServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_vacation", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return acp.api.WriteMessage(req) + return acp.api.Call(req) } // Send the alarm the command for disarm. // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) Disarm(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "alarm_control_panel" - req.Service = "alarm_disarm" + req := BaseServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_disarm", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return acp.api.WriteMessage(req) + return acp.api.Call(req) } // Send the alarm the command for trigger. // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) Trigger(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "alarm_control_panel" - req.Service = "alarm_trigger" + req := BaseServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_trigger", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return acp.api.WriteMessage(req) + return acp.api.Call(req) } diff --git a/internal/services/climate.go b/internal/services/climate.go index 46353e2..43c7156 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -13,19 +13,21 @@ type Climate struct { /* Public API */ func (c Climate) SetFanMode(entityId string, fanMode string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "climate" - req.Service = "set_fan_mode" - req.ServiceData = map[string]any{"fan_mode": fanMode} - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "climate", + Service: "set_fan_mode", + ServiceData: map[string]any{"fan_mode": fanMode}, + Target: Entity(entityId), + } + return c.api.Call(req) } func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatureRequest) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "climate" - req.Service = "set_temperature" - req.ServiceData = serviceData.ToJSON() - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "climate", + Service: "set_temperature", + ServiceData: serviceData.ToJSON(), + Target: Entity(entityId), + } + return c.api.Call(req) } diff --git a/internal/services/cover.go b/internal/services/cover.go index 6b91220..c0ada6c 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -10,98 +10,110 @@ type Cover struct { // Close all or specified cover. Takes an entityId. func (c Cover) Close(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "close_cover" - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "cover", + Service: "close_cover", + Target: Entity(entityId), + } + return c.api.Call(req) } // Close all or specified cover tilt. Takes an entityId. func (c Cover) CloseTilt(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "close_cover_tilt" - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "cover", + Service: "close_cover_tilt", + Target: Entity(entityId), + } + return c.api.Call(req) } // Open all or specified cover. Takes an entityId. func (c Cover) Open(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "open_cover" - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "cover", + Service: "open_cover", + Target: Entity(entityId), + } + return c.api.Call(req) } // Open all or specified cover tilt. Takes an entityId. func (c Cover) OpenTilt(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "open_cover_tilt" - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "cover", + Service: "open_cover_tilt", + Target: Entity(entityId), + } + return c.api.Call(req) } // Move to specific position all or specified cover. Takes an entityId and an optional // map that is translated into service_data. func (c Cover) SetPosition(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "set_cover_position" + req := BaseServiceRequest{ + Domain: "cover", + Service: "set_cover_position", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return c.api.WriteMessage(req) + return c.api.Call(req) } // Move to specific position all or specified cover tilt. Takes an entityId and an optional // map that is translated into service_data. func (c Cover) SetTiltPosition(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "set_cover_tilt_position" + req := BaseServiceRequest{ + Target: Entity(entityId), + Domain: "cover", + Service: "set_cover_tilt_position", + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return c.api.WriteMessage(req) + return c.api.Call(req) } // Stop a cover entity. Takes an entityId. func (c Cover) Stop(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "stop_cover" - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "cover", + Service: "stop_cover", + Target: Entity(entityId), + } + return c.api.Call(req) } // Stop a cover entity tilt. Takes an entityId. func (c Cover) StopTilt(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "stop_cover_tilt" - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "cover", + Service: "stop_cover_tilt", + Target: Entity(entityId), + } + return c.api.Call(req) } // Toggle a cover open/closed. Takes an entityId. func (c Cover) Toggle(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "toggle" - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "cover", + Service: "toggle", + Target: Entity(entityId), + } + return c.api.Call(req) } // Toggle a cover tilt open/closed. Takes an entityId. func (c Cover) ToggleTilt(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "cover" - req.Service = "toggle_cover_tilt" - - return c.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "cover", + Service: "toggle_cover_tilt", + Target: Entity(entityId), + } + return c.api.Call(req) } diff --git a/internal/services/event.go b/internal/services/event.go index c5b5ac1..6747199 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -1,35 +1,16 @@ package services -import ( - "saml.dev/gome-assistant/internal" -) - type Event struct { api API } -// Fire an event -type FireEventRequest struct { - Id int64 `json:"id"` - Type string `json:"type"` // always set to "fire_event" - EventType string `json:"event_type"` - EventData map[string]any `json:"event_data,omitempty"` -} - /* Public API */ // Fire an event. Takes an event type and an optional map that is sent // as `event_data`. func (e Event) Fire(eventType string, eventData ...map[string]any) error { - req := FireEventRequest{ - Id: internal.GetId(), - Type: "fire_event", + if len(eventData) == 0 { + return e.api.FireEvent(eventType, nil) } - - req.EventType = eventType - if len(eventData) != 0 { - req.EventData = eventData[0] - } - - return e.api.WriteMessage(req) + return e.api.FireEvent(eventType, eventData[0]) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 601e9d4..19f93d6 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -7,33 +7,38 @@ type HomeAssistant struct { // TurnOn a Home Assistant entity. Takes an entityId and an optional // map that is translated into service_data. func (ha *HomeAssistant) TurnOn(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "homeassistant" - req.Service = "turn_on" + req := BaseServiceRequest{ + Domain: "homeassistant", + Service: "turn_on", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return ha.api.WriteMessage(req) + return ha.api.Call(req) } // Toggle a Home Assistant entity. Takes an entityId and an optional // map that is translated into service_data. func (ha *HomeAssistant) Toggle(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "homeassistant" - req.Service = "toggle" + req := BaseServiceRequest{ + Domain: "homeassistant", + Service: "toggle", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return ha.api.WriteMessage(req) + return ha.api.Call(req) } func (ha *HomeAssistant) TurnOff(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "homeassistant" - req.Service = "turn_off" - - return ha.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "homeassistant", + Service: "turn_off", + Target: Entity(entityId), + } + return ha.api.Call(req) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index c7df9c7..96d4407 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -9,31 +9,36 @@ type InputBoolean struct { /* Public API */ func (ib InputBoolean) TurnOn(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_boolean" - req.Service = "turn_on" - - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_boolean", + Service: "turn_on", + Target: Entity(entityId), + } + return ib.api.Call(req) } func (ib InputBoolean) Toggle(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_boolean" - req.Service = "toggle" - - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_boolean", + Service: "toggle", + Target: Entity(entityId), + } + return ib.api.Call(req) } func (ib InputBoolean) TurnOff(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_boolean" - req.Service = "turn_off" - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_boolean", + Service: "turn_off", + Target: Entity(entityId), + } + return ib.api.Call(req) } func (ib InputBoolean) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "input_boolean" - req.Service = "reload" - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_boolean", + Service: "reload", + } + return ib.api.Call(req) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 58d7496..3de12db 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -9,16 +9,19 @@ type InputButton struct { /* Public API */ func (ib InputButton) Press(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_button" - req.Service = "press" - - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_button", + Service: "press", + Target: Entity(entityId), + } + return ib.api.Call(req) } func (ib InputButton) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "input_button" - req.Service = "reload" - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_button", + Service: "reload", + Target: Entity(""), + } + return ib.api.Call(req) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 5a17b3f..a83c978 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -14,19 +14,21 @@ type InputDatetime struct { /* Public API */ func (ib InputDatetime) Set(entityId string, value time.Time) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_datetime" - req.Service = "set_datetime" - req.ServiceData = map[string]any{ - "timestamp": fmt.Sprint(value.Unix()), + req := BaseServiceRequest{ + Domain: "input_datetime", + Service: "set_datetime", + ServiceData: map[string]any{ + "timestamp": fmt.Sprint(value.Unix()), + }, + Target: Entity(entityId), } - - return ib.api.WriteMessage(req) + return ib.api.Call(req) } func (ib InputDatetime) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "input_datetime" - req.Service = "reload" - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_datetime", + Service: "reload", + } + return ib.api.Call(req) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 9c82796..ac12761 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -9,33 +9,37 @@ type InputNumber struct { /* Public API */ func (ib InputNumber) Set(entityId string, value float32) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_number" - req.Service = "set_value" - req.ServiceData = map[string]any{"value": value} - - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_number", + Service: "set_value", + ServiceData: map[string]any{"value": value}, + Target: Entity(entityId), + } + return ib.api.Call(req) } func (ib InputNumber) Increment(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_number" - req.Service = "increment" - - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_number", + Service: "increment", + Target: Entity(entityId), + } + return ib.api.Call(req) } func (ib InputNumber) Decrement(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_number" - req.Service = "decrement" - - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_number", + Service: "decrement", + Target: Entity(entityId), + } + return ib.api.Call(req) } func (ib InputNumber) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "input_number" - req.Service = "reload" - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_number", + Service: "reload", + } + return ib.api.Call(req) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 1486b00..1932dc0 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -9,19 +9,21 @@ type InputText struct { /* Public API */ func (ib InputText) Set(entityId string, value string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_text" - req.Service = "set_value" - req.ServiceData = map[string]any{ - "value": value, + req := BaseServiceRequest{ + Domain: "input_text", + Service: "set_value", + ServiceData: map[string]any{ + "value": value, + }, + Target: Entity(entityId), } - - return ib.api.WriteMessage(req) + return ib.api.Call(req) } func (ib InputText) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "input_text" - req.Service = "reload" - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "input_text", + Service: "reload", + } + return ib.api.Call(req) } diff --git a/internal/services/light.go b/internal/services/light.go index 013c69c..e9f7b75 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -11,32 +11,36 @@ type Light struct { // TurnOn a light entity. Takes an entityId and an optional // map that is translated into service_data. func (l Light) TurnOn(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "light" - req.Service = "turn_on" + req := BaseServiceRequest{ + Domain: "light", + Service: "turn_on", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return l.api.WriteMessage(req) + return l.api.Call(req) } // Toggle a light entity. Takes an entityId and an optional // map that is translated into service_data. func (l Light) Toggle(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "light" - req.Service = "toggle" + req := BaseServiceRequest{ + Domain: "light", + Service: "toggle", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return l.api.WriteMessage(req) + return l.api.Call(req) } func (l Light) TurnOff(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "light" - req.Service = "turn_off" - return l.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "light", + Service: "turn_off", + Target: Entity(entityId), + } + return l.api.Call(req) } diff --git a/internal/services/lock.go b/internal/services/lock.go index 2246765..4f80806 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -11,25 +11,27 @@ type Lock struct { // Lock a lock entity. Takes an entityId and an optional // map that is translated into service_data. func (l Lock) Lock(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "lock" - req.Service = "lock" + req := BaseServiceRequest{ + Domain: "lock", + Service: "lock", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return l.api.WriteMessage(req) + return l.api.Call(req) } // Unlock a lock entity. Takes an entityId and an optional // map that is translated into service_data. func (l Lock) Unlock(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "lock" - req.Service = "unlock" + req := BaseServiceRequest{ + Domain: "lock", + Service: "unlock", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return l.api.WriteMessage(req) + return l.api.Call(req) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 7a43066..634c1dd 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -11,255 +11,277 @@ type MediaPlayer struct { // Send the media player the command to clear players playlist. // Takes an entityId. func (mp MediaPlayer) ClearPlaylist(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "clear_playlist" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "clear_playlist", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Group players together. Only works on platforms with support for player groups. // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) Join(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "join" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "join", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Send the media player the command for next track. // Takes an entityId. func (mp MediaPlayer) Next(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "media_next_track" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "media_next_track", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Send the media player the command for pause. // Takes an entityId. func (mp MediaPlayer) Pause(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "media_pause" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "media_pause", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Send the media player the command for play. // Takes an entityId. func (mp MediaPlayer) Play(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "media_play" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "media_play", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Toggle media player play/pause state. // Takes an entityId. func (mp MediaPlayer) PlayPause(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "media_play_pause" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "media_play_pause", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Send the media player the command for previous track. // Takes an entityId. func (mp MediaPlayer) Previous(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "media_previous_track" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "media_previous_track", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Send the media player the command to seek in current playing media. // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) Seek(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "media_seek" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "media_seek", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Send the media player the stop command. // Takes an entityId. func (mp MediaPlayer) Stop(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "media_stop" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "media_stop", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Send the media player the command for playing media. // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) PlayMedia(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "play_media" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "play_media", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Set repeat mode. Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) RepeatSet(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "repeat_set" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "repeat_set", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Send the media player the command to change sound mode. // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "select_sound_mode" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "select_sound_mode", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Send the media player the command to change input source. // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) SelectSource(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "select_source" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "select_source", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Set shuffling state. // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) Shuffle(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "shuffle_set" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "shuffle_set", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Toggles a media player power state. // Takes an entityId. func (mp MediaPlayer) Toggle(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "toggle" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "toggle", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Turn a media player power off. // Takes an entityId. func (mp MediaPlayer) TurnOff(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "turn_off" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "turn_off", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Turn a media player power on. // Takes an entityId. func (mp MediaPlayer) TurnOn(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "turn_on" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "turn_on", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Unjoin the player from a group. Only works on // platforms with support for player groups. // Takes an entityId. func (mp MediaPlayer) Unjoin(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "unjoin" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "unjoin", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Turn a media player volume down. // Takes an entityId. func (mp MediaPlayer) VolumeDown(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "volume_down" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "volume_down", + Target: Entity(entityId), + } + return mp.api.Call(req) } // Mute a media player's volume. // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) VolumeMute(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "volume_mute" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "volume_mute", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Set a media player's volume level. // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) VolumeSet(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "volume_set" + req := BaseServiceRequest{ + Domain: "media_player", + Service: "volume_set", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return mp.api.WriteMessage(req) + return mp.api.Call(req) } // Turn a media player volume up. // Takes an entityId. func (mp MediaPlayer) VolumeUp(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "media_player" - req.Service = "volume_up" - - return mp.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "media_player", + Service: "volume_up", + Target: Entity(entityId), + } + return mp.api.Call(req) } diff --git a/internal/services/notify.go b/internal/services/notify.go index a74d82d..66e29c9 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -10,17 +10,16 @@ type Notify struct { // Notify sends a notification. Takes a types.NotifyRequest. func (ha *Notify) Notify(reqData types.NotifyRequest) error { - req := NewBaseServiceRequest("") - req.Domain = "notify" - req.Service = reqData.ServiceName - + req := BaseServiceRequest{ + Domain: "notify", + Service: reqData.ServiceName, + } serviceData := map[string]any{} serviceData["message"] = reqData.Message serviceData["title"] = reqData.Title if reqData.Data != nil { serviceData["data"] = reqData.Data } - req.ServiceData = serviceData - return ha.api.WriteMessage(req) + return ha.api.Call(req) } diff --git a/internal/services/number.go b/internal/services/number.go index 75d450b..fa4b4dd 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -5,12 +5,13 @@ type Number struct { } func (ib Number) SetValue(entityId string, value float32) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "number" - req.Service = "set_value" - req.ServiceData = map[string]any{"value": value} - - return ib.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "number", + Service: "set_value", + ServiceData: map[string]any{"value": value}, + Target: Entity(entityId), + } + return ib.api.Call(req) } func (ib Number) MustSetValue(entityId string, value float32) { diff --git a/internal/services/scene.go b/internal/services/scene.go index 4455d04..79b29eb 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -10,47 +10,51 @@ type Scene struct { // Apply a scene. Takes map that is translated into service_data. func (s Scene) Apply(serviceData ...map[string]any) error { - req := NewBaseServiceRequest("") - req.Domain = "scene" - req.Service = "apply" + req := BaseServiceRequest{ + Domain: "scene", + Service: "apply", + Target: Entity(""), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return s.api.WriteMessage(req) + return s.api.Call(req) } // Create a scene entity. Takes an entityId and an optional // map that is translated into service_data. func (s Scene) Create(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "scene" - req.Service = "create" + req := BaseServiceRequest{ + Domain: "scene", + Service: "create", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return s.api.WriteMessage(req) + return s.api.Call(req) } // Reload the scenes. func (s Scene) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "scene" - req.Service = "reload" - - return s.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "scene", + Service: "reload", + Target: Entity(""), + } + return s.api.Call(req) } // TurnOn a scene entity. Takes an entityId and an optional // map that is translated into service_data. func (s Scene) TurnOn(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "scene" - req.Service = "turn_on" + req := BaseServiceRequest{ + Domain: "scene", + Service: "turn_on", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return s.api.WriteMessage(req) + return s.api.Call(req) } diff --git a/internal/services/script.go b/internal/services/script.go index 9ec0521..f8de2d6 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -10,36 +10,40 @@ type Script struct { // Reload a script that was created in the HA UI. func (s Script) Reload(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "script" - req.Service = "reload" - - return s.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "script", + Service: "reload", + Target: Entity(entityId), + } + return s.api.Call(req) } // Toggle a script that was created in the HA UI. func (s Script) Toggle(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "script" - req.Service = "toggle" - - return s.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "script", + Service: "toggle", + Target: Entity(entityId), + } + return s.api.Call(req) } // TurnOff a script that was created in the HA UI. func (s Script) TurnOff() error { - req := NewBaseServiceRequest("") - req.Domain = "script" - req.Service = "turn_off" - - return s.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "script", + Service: "turn_off", + Target: Entity(""), + } + return s.api.Call(req) } // TurnOn a script that was created in the HA UI. func (s Script) TurnOn(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "script" - req.Service = "turn_on" - - return s.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "script", + Service: "turn_on", + Target: Entity(entityId), + } + return s.api.Call(req) } diff --git a/internal/services/services.go b/internal/services/services.go index 4980dc6..2348ecc 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -1,13 +1,10 @@ package services -import ( - "saml.dev/gome-assistant/internal" -) - // API is the interface that the individual services use to interact // with HomeAssistant. type API interface { - WriteMessage(msg any) error + Call(req BaseServiceRequest) error + FireEvent(eventType string, eventData map[string]any) error } func BuildService[ @@ -40,23 +37,19 @@ func BuildService[ type BaseServiceRequest struct { Id int64 `json:"id"` - RequestType string `json:"type"` // hardcoded "call_service" + RequestType string `json:"type"` // must be set to "call_service" Domain string `json:"domain"` Service string `json:"service"` ServiceData map[string]any `json:"service_data,omitempty"` - Target struct { - EntityId string `json:"entity_id,omitempty"` - } `json:"target,omitempty"` + Target Target `json:"target,omitempty"` } -func NewBaseServiceRequest(entityId string) BaseServiceRequest { - id := internal.GetId() - bsr := BaseServiceRequest{ - Id: id, - RequestType: "call_service", - } - if entityId != "" { - bsr.Target.EntityId = entityId +type Target struct { + EntityID string `json:"entity_id,omitempty"` +} + +func Entity(entityID string) Target { + return Target{ + EntityID: entityID, } - return bsr } diff --git a/internal/services/switch.go b/internal/services/switch.go index 49a02bb..cde74bc 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -9,25 +9,28 @@ type Switch struct { /* Public API */ func (s Switch) TurnOn(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "switch" - req.Service = "turn_on" - - return s.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "switch", + Service: "turn_on", + Target: Entity(entityId), + } + return s.api.Call(req) } func (s Switch) Toggle(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "switch" - req.Service = "toggle" - - return s.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "switch", + Service: "toggle", + Target: Entity(entityId), + } + return s.api.Call(req) } func (s Switch) TurnOff(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "switch" - req.Service = "turn_off" - - return s.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "switch", + Service: "turn_off", + Target: Entity(entityId), + } + return s.api.Call(req) } diff --git a/internal/services/timer.go b/internal/services/timer.go index f000c24..94b9ef8 100644 --- a/internal/services/timer.go +++ b/internal/services/timer.go @@ -10,56 +10,66 @@ type Timer struct { // See https://www.home-assistant.io/integrations/timer/#action-timerstart func (t Timer) Start(entityId string, duration string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "timer" - req.Service = "start" - req.ServiceData = map[string]any{ - "duration": duration, + req := BaseServiceRequest{ + Domain: "timer", + Service: "start", + ServiceData: map[string]any{ + "duration": duration, + }, + Target: Entity(entityId), } - - return t.api.WriteMessage(req) + return t.api.Call(req) } // See https://www.home-assistant.io/integrations/timer/#action-timerstart func (t Timer) Change(entityId string, duration string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "timer" - req.Service = "change" - req.ServiceData = map[string]any{ - "duration": duration, + req := BaseServiceRequest{ + Domain: "timer", + Service: "change", + ServiceData: map[string]any{ + "duration": duration, + }, + Target: Entity(entityId), } - - return t.api.WriteMessage(req) + return t.api.Call(req) } // See https://www.home-assistant.io/integrations/timer/#action-timerpause func (t Timer) Pause(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "timer" - req.Service = "pause" - return t.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "timer", + Service: "pause", + Target: Entity(entityId), + } + return t.api.Call(req) } // See https://www.home-assistant.io/integrations/timer/#action-timercancel func (t Timer) Cancel() error { - req := NewBaseServiceRequest("") - req.Domain = "timer" - req.Service = "cancel" - return t.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "timer", + Service: "cancel", + Target: Entity(""), + } + return t.api.Call(req) } // See https://www.home-assistant.io/integrations/timer/#action-timerfinish func (t Timer) Finish(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "timer" - req.Service = "finish" - return t.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "timer", + Service: "finish", + Target: Entity(entityId), + } + return t.api.Call(req) } // See https://www.home-assistant.io/integrations/timer/#action-timerreload func (t Timer) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "timer" - req.Service = "reload" - return t.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "timer", + Service: "reload", + Target: Entity(""), + } + return t.api.Call(req) } diff --git a/internal/services/tts.go b/internal/services/tts.go index 8dfcc85..7d1156b 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -10,37 +10,41 @@ type TTS struct { // Remove all text-to-speech cache files and RAM cache. func (tts TTS) ClearCache() error { - req := NewBaseServiceRequest("") - req.Domain = "tts" - req.Service = "clear_cache" - - return tts.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "tts", + Service: "clear_cache", + Target: Entity(""), + } + return tts.api.Call(req) } // Say something using text-to-speech on a media player with cloud. // Takes an entityId and an optional // map that is translated into service_data. func (tts TTS) CloudSay(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "tts" - req.Service = "cloud_say" + req := BaseServiceRequest{ + Domain: "tts", + Service: "cloud_say", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - - return tts.api.WriteMessage(req) + return tts.api.Call(req) } // Say something using text-to-speech on a media player with google_translate. // Takes an entityId and an optional // map that is translated into service_data. func (tts TTS) GoogleTranslateSay(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "tts" - req.Service = "google_translate_say" + req := BaseServiceRequest{ + Domain: "tts", + Service: "google_translate_say", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return tts.api.WriteMessage(req) + return tts.api.Call(req) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 07f68ed..23dcf8d 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -11,116 +11,128 @@ type Vacuum struct { // Tell the vacuum cleaner to do a spot clean-up. // Takes an entityId. func (v Vacuum) CleanSpot(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "clean_spot" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "clean_spot", + Target: Entity(entityId), + } + return v.api.Call(req) } // Locate the vacuum cleaner robot. // Takes an entityId. func (v Vacuum) Locate(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "locate" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "locate", + Target: Entity(entityId), + } + return v.api.Call(req) } // Pause the cleaning task. // Takes an entityId. func (v Vacuum) Pause(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "pause" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "pause", + Target: Entity(entityId), + } + return v.api.Call(req) } // Tell the vacuum cleaner to return to its dock. // Takes an entityId. func (v Vacuum) ReturnToBase(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "return_to_base" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "return_to_base", + Target: Entity(entityId), + } + return v.api.Call(req) } // Send a raw command to the vacuum cleaner. Takes an entityId and an optional // map that is translated into service_data. func (v Vacuum) SendCommand(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "send_command" + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "send_command", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return v.api.WriteMessage(req) + return v.api.Call(req) } // Set the fan speed of the vacuum cleaner. Takes an entityId and an optional // map that is translated into service_data. func (v Vacuum) SetFanSpeed(entityId string, serviceData ...map[string]any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "set_fan_speed" - + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "set_fan_speed", + Target: Entity(entityId), + } if len(serviceData) != 0 { req.ServiceData = serviceData[0] } - return v.api.WriteMessage(req) + return v.api.Call(req) } // Start or resume the cleaning task. // Takes an entityId. func (v Vacuum) Start(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "start" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "start", + Target: Entity(entityId), + } + return v.api.Call(req) } // Start, pause, or resume the cleaning task. // Takes an entityId. func (v Vacuum) StartPause(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "start_pause" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "start_pause", + Target: Entity(entityId), + } + return v.api.Call(req) } // Stop the current cleaning task. // Takes an entityId. func (v Vacuum) Stop(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "stop" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "stop", + Target: Entity(entityId), + } + return v.api.Call(req) } // Stop the current cleaning task and return to home. // Takes an entityId. func (v Vacuum) TurnOff(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "turn_off" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "turn_off", + Target: Entity(entityId), + } + return v.api.Call(req) } // Start a new cleaning task. // Takes an entityId. func (v Vacuum) TurnOn(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "vacuum" - req.Service = "turn_on" - - return v.api.WriteMessage(req) + req := BaseServiceRequest{ + Domain: "vacuum", + Service: "turn_on", + Target: Entity(entityId), + } + return v.api.Call(req) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index e09cde6..c5255d9 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -10,13 +10,14 @@ type ZWaveJS struct { // ZWaveJS bulk_set_partial_config_parameters service. func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, value any) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "zwave_js" - req.Service = "bulk_set_partial_config_parameters" - req.ServiceData = map[string]any{ - "parameter": parameter, - "value": value, + req := BaseServiceRequest{ + Domain: "zwave_js", + Service: "bulk_set_partial_config_parameters", + ServiceData: map[string]any{ + "parameter": parameter, + "value": value, + }, + Target: Entity(entityId), } - - return zw.api.WriteMessage(req) + return zw.api.Call(req) } diff --git a/service.go b/service.go index 049106b..cf973dc 100644 --- a/service.go +++ b/service.go @@ -2,7 +2,6 @@ package gomeassistant import ( "saml.dev/gome-assistant/internal/services" - "saml.dev/gome-assistant/internal/websocket" ) type Service struct { @@ -31,30 +30,30 @@ type Service struct { ZWaveJS *services.ZWaveJS } -func newService(conn *websocket.Conn) *Service { +func newService(app *App) *Service { return &Service{ - AdaptiveLighting: services.BuildService[services.AdaptiveLighting](conn), - AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn), - Climate: services.BuildService[services.Climate](conn), - Cover: services.BuildService[services.Cover](conn), - Light: services.BuildService[services.Light](conn), - HomeAssistant: services.BuildService[services.HomeAssistant](conn), - Lock: services.BuildService[services.Lock](conn), - MediaPlayer: services.BuildService[services.MediaPlayer](conn), - Switch: services.BuildService[services.Switch](conn), - InputBoolean: services.BuildService[services.InputBoolean](conn), - InputButton: services.BuildService[services.InputButton](conn), - InputText: services.BuildService[services.InputText](conn), - InputDatetime: services.BuildService[services.InputDatetime](conn), - InputNumber: services.BuildService[services.InputNumber](conn), - Event: services.BuildService[services.Event](conn), - Notify: services.BuildService[services.Notify](conn), - Number: services.BuildService[services.Number](conn), - Scene: services.BuildService[services.Scene](conn), - Script: services.BuildService[services.Script](conn), - Timer: services.BuildService[services.Timer](conn), - TTS: services.BuildService[services.TTS](conn), - Vacuum: services.BuildService[services.Vacuum](conn), - ZWaveJS: services.BuildService[services.ZWaveJS](conn), + AdaptiveLighting: services.BuildService[services.AdaptiveLighting](app), + AlarmControlPanel: services.BuildService[services.AlarmControlPanel](app), + Climate: services.BuildService[services.Climate](app), + Cover: services.BuildService[services.Cover](app), + Light: services.BuildService[services.Light](app), + HomeAssistant: services.BuildService[services.HomeAssistant](app), + Lock: services.BuildService[services.Lock](app), + MediaPlayer: services.BuildService[services.MediaPlayer](app), + Switch: services.BuildService[services.Switch](app), + InputBoolean: services.BuildService[services.InputBoolean](app), + InputButton: services.BuildService[services.InputButton](app), + InputText: services.BuildService[services.InputText](app), + InputDatetime: services.BuildService[services.InputDatetime](app), + InputNumber: services.BuildService[services.InputNumber](app), + Event: services.BuildService[services.Event](app), + Notify: services.BuildService[services.Notify](app), + Number: services.BuildService[services.Number](app), + Scene: services.BuildService[services.Scene](app), + Script: services.BuildService[services.Script](app), + Timer: services.BuildService[services.Timer](app), + TTS: services.BuildService[services.TTS](app), + Vacuum: services.BuildService[services.Vacuum](app), + ZWaveJS: services.BuildService[services.ZWaveJS](app), } } From 864d1452e88b1c4765416aee1cf4a8711c107204 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 5 Dec 2025 23:04:27 +0100 Subject: [PATCH 6/7] Conn.NextMessageID(): new method Instead of using a global function `internal.GetId()` to generate message IDs, make this a feature of `websocket.Conn`. This makes more logical sense, and it will also make it possible to fix a race in the next commit. --- app.go | 2 +- call.go | 3 +-- fire_event.go | 6 +----- internal/internal.go | 7 ------- internal/websocket/websocket.go | 19 ++++++++++++++----- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/app.go b/app.go index 48bdbe2..361f6e0 100644 --- a/app.go +++ b/app.go @@ -316,7 +316,7 @@ func (app *App) Start() { go app.runScheduledActions(app.ctx) // subscribe to state_changed events - id := internal.GetId() + id := app.conn.NextMessageID() websocket.SubscribeToStateChangedEvents(id, app.conn) app.entityListenersId = id diff --git a/call.go b/call.go index 03b8e4b..7346c39 100644 --- a/call.go +++ b/call.go @@ -1,12 +1,11 @@ package gomeassistant import ( - "saml.dev/gome-assistant/internal" "saml.dev/gome-assistant/internal/services" ) func (app *App) Call(req services.BaseServiceRequest) error { req.RequestType = "call_service" - req.Id = internal.GetId() + req.Id = app.conn.NextMessageID() return app.conn.WriteMessage(req) } diff --git a/fire_event.go b/fire_event.go index 913d690..950d583 100644 --- a/fire_event.go +++ b/fire_event.go @@ -1,12 +1,8 @@ package gomeassistant -import ( - "saml.dev/gome-assistant/internal" -) - func (app *App) FireEvent(eventType string, eventData map[string]any) error { req := FireEventRequest{ - Id: internal.GetId(), + Id: app.conn.NextMessageID(), Type: "fire_event", EventType: eventType, EventData: eventData, diff --git a/internal/internal.go b/internal/internal.go index f5efc14..2e818cc 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -16,13 +16,6 @@ type EnabledDisabledInfo struct { RunOnError bool } -var id int64 = 0 - -func GetId() int64 { - id += 1 - return id -} - // Parses a HH:MM string. func ParseTime(s string) carbon.Carbon { t, err := time.Parse("15:04", s) diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 453485a..982e99f 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -14,8 +14,6 @@ import ( "sync" "github.com/gorilla/websocket" - - "saml.dev/gome-assistant/internal" ) var ErrInvalidToken = errors.New("invalid authentication token") @@ -26,8 +24,19 @@ type AuthMessage struct { } type Conn struct { - conn *websocket.Conn - writeLock sync.Mutex + conn *websocket.Conn + writeLock sync.Mutex + lastMessageID int64 +} + +// NextMessageID returns the next ID in the sequence used for message +// numbers. These IDs must be used in numerical order! +func (conn *Conn) NextMessageID() int64 { + conn.writeLock.Lock() + defer conn.writeLock.Unlock() + + conn.lastMessageID++ + return conn.lastMessageID } func (conn *Conn) WriteMessage(msg any) error { @@ -142,7 +151,7 @@ func SubscribeToStateChangedEvents(id int64, conn *Conn) { func SubscribeToEventType(eventType string, conn *Conn, id ...int64) { var finalId int64 if len(id) == 0 { - finalId = internal.GetId() + finalId = conn.NextMessageID() } else { finalId = id[0] } From ff873560d096e71a6b6b15e21abd59164c6f1d2f Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 5 Dec 2025 23:33:58 +0100 Subject: [PATCH 7/7] Ensure that message IDs are used in the correct order The websocket API insists that message IDs be monotonically increasing (i.e., not just unique). That means that we need to hold a lock the whole time from when we allocate the message ID until we send the message. Change the interface for sending messages to make this safe: * Move the "write" methods of `Conn` to a new type, `LockedConn`. * Add a method `Conn.Send()`, which takes a callback function as an argument. The callback gets a `LockedConn` as an argument. (This is the only way to get a hold of a `LockedConn`.) The callback is invoked while `Conn.writeLock` is held. The callback can thus allocate new message IDs and send messages with those IDs without fear that another thread interferes with it. --- app.go | 16 +++---- call.go | 10 ++++- fire_event.go | 20 ++++++--- internal/websocket/locked_conn.go | 40 +++++++++++++++++ internal/websocket/send.go | 31 +++++++++++++ internal/websocket/websocket.go | 74 +++++++++++++++---------------- 6 files changed, 136 insertions(+), 55 deletions(-) create mode 100644 internal/websocket/locked_conn.go create mode 100644 internal/websocket/send.go diff --git a/app.go b/app.go index 361f6e0..dbd512f 100644 --- a/app.go +++ b/app.go @@ -38,11 +38,11 @@ type App struct { service *Service state *StateImpl - scheduledActions []scheduledAction - scheduleCount int - entityListeners map[string][]*EntityListener - entityListenersId int64 - eventListeners map[string][]*EventListener + scheduledActions []scheduledAction + scheduleCount int + entityListeners map[string][]*EntityListener + entitySubscription websocket.Subscription + eventListeners map[string][]*EventListener } // DurationString represents a duration, such as "2s" or "24h". @@ -316,9 +316,7 @@ func (app *App) Start() { go app.runScheduledActions(app.ctx) // subscribe to state_changed events - id := app.conn.NextMessageID() - websocket.SubscribeToStateChangedEvents(id, app.conn) - app.entityListenersId = id + app.entitySubscription = websocket.SubscribeToStateChangedEvents(app.conn) // entity listeners runOnStartup for eid, etls := range app.entityListeners { @@ -353,7 +351,7 @@ func (app *App) Start() { if !ok { break } - if app.entityListenersId == msg.Id { + if app.entitySubscription.ID() == msg.Id { go callEntityListeners(app, msg.Raw) } else { go callEventListeners(app, msg) diff --git a/call.go b/call.go index 7346c39..c3c61fa 100644 --- a/call.go +++ b/call.go @@ -2,10 +2,16 @@ package gomeassistant import ( "saml.dev/gome-assistant/internal/services" + "saml.dev/gome-assistant/internal/websocket" ) func (app *App) Call(req services.BaseServiceRequest) error { req.RequestType = "call_service" - req.Id = app.conn.NextMessageID() - return app.conn.WriteMessage(req) + + return app.conn.Send( + func(lc websocket.LockedConn) error { + req.Id = lc.NextMessageID() + return lc.SendMessage(req) + }, + ) } diff --git a/fire_event.go b/fire_event.go index 950d583..148cc79 100644 --- a/fire_event.go +++ b/fire_event.go @@ -1,14 +1,20 @@ package gomeassistant +import "saml.dev/gome-assistant/internal/websocket" + func (app *App) FireEvent(eventType string, eventData map[string]any) error { - req := FireEventRequest{ - Id: app.conn.NextMessageID(), - Type: "fire_event", - EventType: eventType, - EventData: eventData, - } + return app.conn.Send( + func(lc websocket.LockedConn) error { + req := FireEventRequest{ + Id: lc.NextMessageID(), + Type: "fire_event", + EventType: eventType, + EventData: eventData, + } - return app.conn.WriteMessage(req) + return lc.SendMessage(req) + }, + ) } // Fire an event diff --git a/internal/websocket/locked_conn.go b/internal/websocket/locked_conn.go new file mode 100644 index 0000000..8557b71 --- /dev/null +++ b/internal/websocket/locked_conn.go @@ -0,0 +1,40 @@ +package websocket + +import "fmt" + +// LockedConn represents a `Conn` that is currently locked for +// writing. It is created within [Conn.Send] for the use of that +// method's callback function. +type LockedConn interface { + // NextMessageID returns the next unused id to be used in a websocket + // message. The IDs so generated must be used in order, while the + // `LockedConn` is still active. + NextMessageID() int64 + + // SendMessage sends the specified message over the websocket + // connection. `msg` must be JSON-serializable and have the + // correct format and a unique, monotonically-increasing ID, which + // should be generated using `NextMessageID()` and used in order. + SendMessage(msg any) error +} + +// lockedConn is a `LockedConn` view of a `Conn`, to be used +// only for a finite time when the connection is locked. +type lockedConn struct { + conn *Conn +} + +// NextMessageID implements [LockedConn.NextMessageID]. +func (lc lockedConn) NextMessageID() int64 { + lc.conn.lastMessageID++ + return lc.conn.lastMessageID +} + +// SendMessage implements [LockedConn.SendMessage]. +func (lc lockedConn) SendMessage(msg any) error { + if err := lc.conn.conn.WriteJSON(msg); err != nil { + return fmt.Errorf("sending websocket message to server: %w", err) + } + + return nil +} diff --git a/internal/websocket/send.go b/internal/websocket/send.go new file mode 100644 index 0000000..e892427 --- /dev/null +++ b/internal/websocket/send.go @@ -0,0 +1,31 @@ +package websocket + +// Messager is called by `Send()` while holding the `writeMutex`. It +// can send one or more messages by (for each message) allocating an +// ID using `lc.NextMessageID()` then sending the message using +// `lc.SendMessage()`. The `LockedConn` should only be used while the +// callback is running. +type Messager func(lc LockedConn) error + +// Send is the primary way to write a message over the websocket +// interface. Since these messages require monotonically-increasing ID +// numbers, the work from allocating a new ID number through sending +// the message has to be done under the `writeMutex`. This is done by +// passing this function a `Messager`, which is invoked while holding +// the lock. +// +// Usage: +// +// msg := NewFooMessage{…} +// err := conn.Send(func(lc LockedConn) error { +// id := lc.NextMessageID() +// // …do anything else that needs to be done with `id`… +// msg.ID = id +// return lc.SendMessage(msg) +// }) +func (conn *Conn) Send(msgr Messager) error { + conn.writeLock.Lock() + defer conn.writeLock.Unlock() + + return msgr(lockedConn{conn: conn}) +} diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 982e99f..b91445f 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -29,23 +29,6 @@ type Conn struct { lastMessageID int64 } -// NextMessageID returns the next ID in the sequence used for message -// numbers. These IDs must be used in numerical order! -func (conn *Conn) NextMessageID() int64 { - conn.writeLock.Lock() - defer conn.writeLock.Unlock() - - conn.lastMessageID++ - return conn.lastMessageID -} - -func (conn *Conn) WriteMessage(msg any) error { - conn.writeLock.Lock() - defer conn.writeLock.Unlock() - - return conn.conn.WriteJSON(msg) -} - func (conn *Conn) readMessage() ([]byte, error) { _, msg, err := conn.conn.ReadMessage() if err != nil { @@ -144,28 +127,45 @@ type SubEvent struct { EventType string `json:"event_type"` } -func SubscribeToStateChangedEvents(id int64, conn *Conn) { - SubscribeToEventType("state_changed", conn, id) +// Subscription represents a websocket-level subscription to a +// particular message ID. +type Subscription struct { + id int64 } -func SubscribeToEventType(eventType string, conn *Conn, id ...int64) { - var finalId int64 - if len(id) == 0 { - finalId = conn.NextMessageID() - } else { - finalId = id[0] - } - e := SubEvent{ - Id: finalId, - Type: "subscribe_events", - EventType: eventType, - } - err := conn.WriteMessage(e) +func (sub Subscription) ID() int64 { + return sub.id +} + +func SubscribeToStateChangedEvents(conn *Conn) Subscription { + return SubscribeToEventType("state_changed", conn) +} + +func SubscribeToEventType(eventType string, conn *Conn) Subscription { + var id int64 + err := conn.Send( + func(lc LockedConn) error { + id = lc.NextMessageID() + e := SubEvent{ + Id: id, + Type: "subscribe_events", + EventType: eventType, + } + + if err := lc.SendMessage(e); err != nil { + return fmt.Errorf("error writing to websocket: %w", err) + } + // m, _ := ReadMessage(ctx, conn) + // log.Default().Println(string(m)) + + return nil + }, + ) + if err != nil { - wrappedErr := fmt.Errorf("error writing to websocket: %w", err) - slog.Error(wrappedErr.Error()) - panic(wrappedErr) + slog.Error(err.Error()) + panic(err) } - // m, _ := ReadMessage(ctx, conn) - // log.Default().Println(string(m)) + + return Subscription{id} }