diff --git a/app.go b/app.go index 150ce60..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". @@ -160,43 +160,46 @@ 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 (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 +212,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 +229,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 +242,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(eventType, app.conn) + app.eventListeners[eventType] = []*EventListener{&evl} } } } @@ -295,41 +298,39 @@ 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 + app.entitySubscription = websocket.SubscribeToStateChangedEvents(app.conn) // 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 +344,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.entitySubscription.ID() == msg.Id { + go callEntityListeners(app, msg.Raw) } else { - go callEventListeners(a, msg) + go callEventListeners(app, msg) } } } @@ -362,23 +363,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/call.go b/call.go new file mode 100644 index 0000000..c3c61fa --- /dev/null +++ b/call.go @@ -0,0 +1,17 @@ +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" + + 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 new file mode 100644 index 0000000..148cc79 --- /dev/null +++ b/fire_event.go @@ -0,0 +1,26 @@ +package gomeassistant + +import "saml.dev/gome-assistant/internal/websocket" + +func (app *App) FireEvent(eventType string, eventData map[string]any) error { + return app.conn.Send( + func(lc websocket.LockedConn) error { + req := FireEventRequest{ + Id: lc.NextMessageID(), + Type: "fire_event", + EventType: eventType, + EventData: eventData, + } + + return lc.SendMessage(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/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/services/adaptive_lighting.go b/internal/services/adaptive_lighting.go index abfb29f..c09485b 100644 --- a/internal/services/adaptive_lighting.go +++ b/internal/services/adaptive_lighting.go @@ -1,26 +1,24 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type AdaptiveLighting struct { - conn *websocket.Conn + api API } /* Public API */ // 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.conn.WriteMessage(req) + return al.api.Call(req) } diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 3b8858e..fa02e65 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 */ @@ -16,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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.WriteMessage(req) + return acp.api.Call(req) } diff --git a/internal/services/climate.go b/internal/services/climate.go index 74d167d..43c7156 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -1,32 +1,33 @@ 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 */ 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.conn.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.conn.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 4a71893..c0ada6c 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -1,111 +1,119 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Cover struct { - conn *websocket.Conn + api API } /* Public API */ // 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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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 67ff39d..6747199 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -1,20 +1,7 @@ package services -import ( - "saml.dev/gome-assistant/internal" - "saml.dev/gome-assistant/internal/websocket" -) - type Event struct { - conn *websocket.Conn -} - -// 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"` + api API } /* Public API */ @@ -22,15 +9,8 @@ type FireEventRequest struct { // 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.conn.WriteMessage(req) + return e.api.FireEvent(eventType, eventData[0]) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index d888f85..19f93d6 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -1,43 +1,44 @@ 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 // 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.conn.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.conn.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.conn.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 788b830..96d4407 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -1,43 +1,44 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type InputBoolean struct { - conn *websocket.Conn + api API } /* Public API */ func (ib InputBoolean) TurnOn(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_boolean" - req.Service = "turn_on" - - return ib.conn.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.conn.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.conn.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.conn.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 7a20657..3de12db 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -1,28 +1,27 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type InputButton struct { - conn *websocket.Conn + api API } /* Public API */ func (ib InputButton) Press(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "input_button" - req.Service = "press" - - return ib.conn.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.conn.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 0306838..a83c978 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -3,32 +3,32 @@ package services import ( "fmt" "time" - - "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputDatetime struct { - conn *websocket.Conn + api API } /* 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.conn.WriteMessage(req) + return ib.api.Call(req) } func (ib InputDatetime) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "input_datetime" - req.Service = "reload" - return ib.conn.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 596285f..ac12761 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -1,45 +1,45 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type InputNumber struct { - conn *websocket.Conn + api API } /* 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.conn.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.conn.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.conn.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.conn.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 ed6789e..1932dc0 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -1,31 +1,29 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type InputText struct { - conn *websocket.Conn + api API } /* 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.conn.WriteMessage(req) + return ib.api.Call(req) } func (ib InputText) Reload() error { - req := NewBaseServiceRequest("") - req.Domain = "input_text" - req.Service = "reload" - return ib.conn.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 dedb9b1..e9f7b75 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 */ @@ -15,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.conn.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.conn.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.conn.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 f6e831a..4f80806 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 */ @@ -15,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.conn.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.conn.WriteMessage(req) + return l.api.Call(req) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index bc64499..634c1dd 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 */ @@ -15,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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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 22047fc..66e29c9 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -1,27 +1,25 @@ 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. 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.conn.WriteMessage(req) + return ha.api.Call(req) } diff --git a/internal/services/number.go b/internal/services/number.go index 8a215b2..fa4b4dd 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -1,20 +1,17 @@ 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 { - req := NewBaseServiceRequest(entityId) - req.Domain = "number" - req.Service = "set_value" - req.ServiceData = map[string]any{"value": value} - - return ib.conn.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 ed729a2..79b29eb 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -1,60 +1,60 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Scene struct { - conn *websocket.Conn + api API } /* Public API */ // 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.conn.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.conn.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.conn.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.conn.WriteMessage(req) + return s.api.Call(req) } diff --git a/internal/services/script.go b/internal/services/script.go index dbe6d0f..f8de2d6 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -1,49 +1,49 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Script struct { - conn *websocket.Conn + api API } /* Public API */ // 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.conn.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.conn.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.conn.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.conn.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 80bc564..2348ecc 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -1,9 +1,11 @@ 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 { + Call(req BaseServiceRequest) error + FireEvent(eventType string, eventData map[string]any) error +} func BuildService[ T AdaptiveLighting | @@ -29,29 +31,25 @@ func BuildService[ Timer | Vacuum | ZWaveJS, -](conn *websocket.Conn) *T { - return &T{conn: conn} +](api API) *T { + return &T{api: api} } 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 dee37ee..cde74bc 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -1,37 +1,36 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Switch struct { - conn *websocket.Conn + api API } /* Public API */ func (s Switch) TurnOn(entityId string) error { - req := NewBaseServiceRequest(entityId) - req.Domain = "switch" - req.Service = "turn_on" - - return s.conn.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.conn.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.conn.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 0dfc70d..94b9ef8 100644 --- a/internal/services/timer.go +++ b/internal/services/timer.go @@ -1,69 +1,75 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type Timer struct { - conn *websocket.Conn + api API } /* Public API */ // 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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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 60fcf85..7d1156b 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -1,50 +1,50 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type TTS struct { - conn *websocket.Conn + api API } /* Public API */ // 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.conn.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.conn.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.conn.WriteMessage(req) + return tts.api.Call(req) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 1d1c136..23dcf8d 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 */ @@ -15,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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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.conn.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 9baf96c..c5255d9 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -1,26 +1,23 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) - /* Structs */ type ZWaveJS struct { - conn *websocket.Conn + api API } /* Public API */ // 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.conn.WriteMessage(req) + return zw.api.Call(req) } 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 3b1376e..b91445f 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,15 +24,9 @@ type AuthMessage struct { } type Conn struct { - conn *websocket.Conn - mutex sync.Mutex -} - -func (conn *Conn) WriteMessage(msg any) error { - conn.mutex.Lock() - defer conn.mutex.Unlock() - - return conn.conn.WriteJSON(msg) + conn *websocket.Conn + writeLock sync.Mutex + lastMessageID int64 } func (conn *Conn) readMessage() ([]byte, error) { @@ -135,28 +127,45 @@ type SubEvent struct { EventType string `json:"event_type"` } -func SubscribeToStateChangedEvents(ctx context.Context, id int64, conn *Conn) { - SubscribeToEventType(ctx, "state_changed", conn, id) +// Subscription represents a websocket-level subscription to a +// particular message ID. +type Subscription struct { + id int64 } -func SubscribeToEventType(ctx context.Context, eventType string, conn *Conn, id ...int64) { - var finalId int64 - if len(id) == 0 { - finalId = internal.GetId() - } 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} } 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() 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), } }