From 087e53009d81e033a3b07566e04fe673e2076e32 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 24 Mar 2024 00:22:21 +0100 Subject: [PATCH 001/103] NewAppFromConfig(): more flexible way to configure an `App` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old code always created URIs with the following patterns: * `http[s]://{ip}:{port}/api/[…]` * `ws[s]://{ip}:{port}/api/websocket` where `{ip}` and `{port}` are externally-specified parameters. But if, for example, the code is running within a supervised container, then it is supposed to connect via the proxy at * `http://supervisor/core/api/[…]` * `ws://supervisor/core/websocket` or `ws://supervisor/core/api/websocket` It is not possible to generate such URIs with the existing code (because of the extra `core/` in the path). So instead, continue a little bit further with the idea of https://github.com/saml-dev/gome-assistant/pull/17: add a second way to initialize the app, where the caller can specify the URIs itself. This is implemented via a new constructor function `NewAppFromConfig()`, which takes a new configuration type, `NewAppConfig`, as argument. Rewrite `NewApp()` to call `NewAppFromConfig()`. --- app.go | 119 ++++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 39 deletions(-) diff --git a/app.go b/app.go index 4cd6e79..2c78edd 100644 --- a/app.go +++ b/app.go @@ -57,6 +57,78 @@ type timeRange struct { end time.Time } +type NewAppConfig struct { + // RESTBaseURI is the base URI for REST requests; for example, + // * `http://homeassistant.local:8123/api` from outside of the + // HA appliance (without encryption) + // * `https://homeassistant.local:8123/api` from outside of the + // HA appliance (with encryption) + // * `http://supervisor/core/api` from an add-on running within + // the appliance and connecting via the proxy + RESTBaseURI string + + // WebsocketURI is the base URI for websocket connections; for + // example, + // * `ws://homeassistant.local:8123/api/websocket` from outside + // of the HA appliance (without encryption) + // * `wss://homeassistant.local:8123/api/websocket` from outside + // of the HA appliance (with encryption) + // * `ws://supervisor/core/api/websocket` from an add-on running + // within the appliance and connecting via the proxy + WebsocketURI string + + // Auth token generated in Home Assistant. Used to connect to the + // Websocket API. + HAAuthToken string + + // Required + // EntityId of the zone representing your home e.g. "zone.home". + // Used to pull latitude/longitude from Home Assistant + // to calculate sunset/sunrise times. + HomeZoneEntityId string +} + +/* +NewAppFromConfig establishes the websocket connection and returns an +object you can use to register schedules and listeners, based on the +URIs that it should connect to. +*/ +func NewAppFromConfig(config NewAppConfig) (*App, error) { + if config.RESTBaseURI == "" || config.WebsocketURI == "" || + config.HAAuthToken == "" || config.HomeZoneEntityId == "" { + slog.Error("RESTBaseURI, WebsocketURI, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") + return nil, ErrInvalidArgs + } + + conn, ctx, ctxCancel, err := ws.ConnectionFromUri(config.WebsocketURI, config.HAAuthToken) + if err != nil { + return nil, err + } + + httpClient := http.ClientFromUri(config.RESTBaseURI, config.HAAuthToken) + + wsWriter := &ws.WebsocketWriter{Conn: conn} + service := newService(wsWriter, ctx, httpClient) + state, err := newState(httpClient, config.HomeZoneEntityId) + if err != nil { + return nil, err + } + + return &App{ + conn: conn, + wsWriter: wsWriter, + ctx: ctx, + ctxCancel: ctxCancel, + httpClient: httpClient, + service: service, + state: state, + schedules: pq.New(), + intervals: pq.New(), + entityListeners: map[string][]*EntityListener{}, + eventListeners: map[string][]*EventListener{}, + }, nil +} + type NewAppRequest struct { // Required // IpAddress of your Home Assistant instance i.e. "localhost" @@ -99,51 +171,20 @@ func NewApp(request NewAppRequest) (*App, error) { port = "8123" } - var ( - conn *websocket.Conn - ctx context.Context - ctxCancel context.CancelFunc - err error - ) - - if request.Secure { - conn, ctx, ctxCancel, err = ws.SetupSecureConnection(request.IpAddress, port, request.HAAuthToken) - } else { - conn, ctx, ctxCancel, err = ws.SetupConnection(request.IpAddress, port, request.HAAuthToken) - } - - if conn == nil { - return nil, err + config := NewAppConfig{ + HAAuthToken: request.HAAuthToken, + HomeZoneEntityId: request.HomeZoneEntityId, } - var httpClient *http.HttpClient - if request.Secure { - httpClient = http.NewHttpsClient(request.IpAddress, port, request.HAAuthToken) + config.WebsocketURI = fmt.Sprintf("wss://%s:%s/api/websocket", request.IpAddress, port) + config.RESTBaseURI = fmt.Sprintf("https://%s:%s/api", request.IpAddress, port) } else { - httpClient = http.NewHttpClient(request.IpAddress, port, request.HAAuthToken) + config.WebsocketURI = fmt.Sprintf("ws://%s:%s/api/websocket", request.IpAddress, port) + config.RESTBaseURI = fmt.Sprintf("http://%s:%s/api", request.IpAddress, port) } - wsWriter := &ws.WebsocketWriter{Conn: conn} - service := newService(wsWriter, ctx, httpClient) - state, err := newState(httpClient, request.HomeZoneEntityId) - if err != nil { - return nil, err - } - - return &App{ - conn: conn, - wsWriter: wsWriter, - ctx: ctx, - ctxCancel: ctxCancel, - httpClient: httpClient, - service: service, - state: state, - schedules: pq.New(), - intervals: pq.New(), - entityListeners: map[string][]*EntityListener{}, - eventListeners: map[string][]*EventListener{}, - }, nil + return NewAppFromConfig(config) } func (a *App) Cleanup() { From 5f407ed55c12689739f2f4853da6fdbaaec99327 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 11:30:32 +0100 Subject: [PATCH 002/103] `ctx` should always be a function's first argument See https://pkg.go.dev/context --- app.go | 8 ++--- internal/services/alarm_control_panel.go | 14 ++++---- internal/services/climate.go | 4 +-- internal/services/cover.go | 20 +++++------ internal/services/event.go | 2 +- internal/services/homeassistant.go | 6 ++-- internal/services/input_boolean.go | 8 ++--- internal/services/input_button.go | 4 +-- internal/services/input_datetime.go | 4 +-- internal/services/input_number.go | 8 ++--- internal/services/input_text.go | 4 +-- internal/services/light.go | 6 ++-- internal/services/lock.go | 4 +-- internal/services/media_player.go | 44 ++++++++++++------------ internal/services/notify.go | 2 +- internal/services/number.go | 2 +- internal/services/scene.go | 8 ++--- internal/services/script.go | 8 ++--- internal/services/services.go | 2 +- internal/services/switch.go | 6 ++-- internal/services/tts.go | 6 ++-- internal/services/vacuum.go | 22 ++++++------ internal/services/zwavejs.go | 2 +- internal/websocket/reader.go | 4 +-- internal/websocket/websocket.go | 24 ++++++------- service.go | 44 ++++++++++++------------ 26 files changed, 133 insertions(+), 133 deletions(-) diff --git a/app.go b/app.go index 2c78edd..8dc492d 100644 --- a/app.go +++ b/app.go @@ -108,7 +108,7 @@ func NewAppFromConfig(config NewAppConfig) (*App, error) { httpClient := http.ClientFromUri(config.RESTBaseURI, config.HAAuthToken) wsWriter := &ws.WebsocketWriter{Conn: conn} - service := newService(wsWriter, ctx, httpClient) + service := newService(ctx, wsWriter, httpClient) state, err := newState(httpClient, config.HomeZoneEntityId) if err != nil { return nil, err @@ -256,7 +256,7 @@ func (a *App) RegisterEventListeners(evls ...EventListener) { if elList, ok := a.eventListeners[eventType]; ok { a.eventListeners[eventType] = append(elList, &evl) } else { - ws.SubscribeToEventType(eventType, a.wsWriter, a.ctx) + ws.SubscribeToEventType(a.ctx, eventType, a.wsWriter) a.eventListeners[eventType] = []*EventListener{&evl} } } @@ -316,7 +316,7 @@ func (a *App) Start() { // subscribe to state_changed events id := internal.GetId() - ws.SubscribeToStateChangedEvents(id, a.wsWriter, a.ctx) + ws.SubscribeToStateChangedEvents(a.ctx, id, a.wsWriter) a.entityListenersId = id // entity listeners runOnStartup @@ -345,7 +345,7 @@ func (a *App) Start() { // entity listeners and event listeners elChan := make(chan ws.ChanMsg) - go ws.ListenWebsocket(a.conn, a.ctx, elChan) + go ws.ListenWebsocket(a.ctx, a.conn, elChan) for { msg, ok := <-elChan diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 5b0756e..38e5c0d 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -26,7 +26,7 @@ func (acp AlarmControlPanel) ArmAway(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req, acp.ctx) + acp.conn.WriteMessage(acp.ctx, req) } // Send the alarm the command for arm away. @@ -40,7 +40,7 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData .. req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req, acp.ctx) + acp.conn.WriteMessage(acp.ctx, req) } // Send the alarm the command for arm home. @@ -54,7 +54,7 @@ func (acp AlarmControlPanel) ArmHome(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req, acp.ctx) + acp.conn.WriteMessage(acp.ctx, req) } // Send the alarm the command for arm night. @@ -68,7 +68,7 @@ func (acp AlarmControlPanel) ArmNight(entityId string, serviceData ...map[string req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req, acp.ctx) + acp.conn.WriteMessage(acp.ctx, req) } // Send the alarm the command for arm vacation. @@ -82,7 +82,7 @@ func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData ...map[str req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req, acp.ctx) + acp.conn.WriteMessage(acp.ctx, req) } // Send the alarm the command for disarm. @@ -96,7 +96,7 @@ func (acp AlarmControlPanel) Disarm(entityId string, serviceData ...map[string]a req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req, acp.ctx) + acp.conn.WriteMessage(acp.ctx, req) } // Send the alarm the command for trigger. @@ -110,5 +110,5 @@ func (acp AlarmControlPanel) Trigger(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req, acp.ctx) + acp.conn.WriteMessage(acp.ctx, req) } diff --git a/internal/services/climate.go b/internal/services/climate.go index 797215f..d37460f 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -22,7 +22,7 @@ func (c Climate) SetFanMode(entityId string, fanMode string) { req.Service = "set_fan_mode" req.ServiceData = map[string]any{"fan_mode": fanMode} - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatureRequest) { @@ -31,5 +31,5 @@ func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatur req.Service = "set_temperature" req.ServiceData = serviceData.ToJSON() - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } diff --git a/internal/services/cover.go b/internal/services/cover.go index 8fb6e75..83b0d83 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -21,7 +21,7 @@ func (c Cover) Close(entityId string) { req.Domain = "cover" req.Service = "close_cover" - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Close all or specified cover tilt. Takes an entityId. @@ -30,7 +30,7 @@ func (c Cover) CloseTilt(entityId string) { req.Domain = "cover" req.Service = "close_cover_tilt" - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Open all or specified cover. Takes an entityId. @@ -39,7 +39,7 @@ func (c Cover) Open(entityId string) { req.Domain = "cover" req.Service = "open_cover" - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Open all or specified cover tilt. Takes an entityId. @@ -48,7 +48,7 @@ func (c Cover) OpenTilt(entityId string) { req.Domain = "cover" req.Service = "open_cover_tilt" - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Move to specific position all or specified cover. Takes an entityId and an optional @@ -61,7 +61,7 @@ func (c Cover) SetPosition(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Move to specific position all or specified cover tilt. Takes an entityId and an optional @@ -74,7 +74,7 @@ func (c Cover) SetTiltPosition(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Stop a cover entity. Takes an entityId. @@ -83,7 +83,7 @@ func (c Cover) Stop(entityId string) { req.Domain = "cover" req.Service = "stop_cover" - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Stop a cover entity tilt. Takes an entityId. @@ -92,7 +92,7 @@ func (c Cover) StopTilt(entityId string) { req.Domain = "cover" req.Service = "stop_cover_tilt" - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Toggle a cover open/closed. Takes an entityId. @@ -101,7 +101,7 @@ func (c Cover) Toggle(entityId string) { req.Domain = "cover" req.Service = "toggle" - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } // Toggle a cover tilt open/closed. Takes an entityId. @@ -110,5 +110,5 @@ func (c Cover) ToggleTilt(entityId string) { req.Domain = "cover" req.Service = "toggle_cover_tilt" - c.conn.WriteMessage(req, c.ctx) + c.conn.WriteMessage(c.ctx, req) } diff --git a/internal/services/event.go b/internal/services/event.go index 9205db1..d347cd0 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -35,5 +35,5 @@ func (e Event) Fire(eventType string, eventData ...map[string]any) { req.EventData = eventData[0] } - e.conn.WriteMessage(req, e.ctx) + e.conn.WriteMessage(e.ctx, req) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 8400509..f7b499d 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -21,7 +21,7 @@ func (ha *HomeAssistant) TurnOn(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - ha.conn.WriteMessage(req, ha.ctx) + ha.conn.WriteMessage(ha.ctx, req) } // Toggle a Home Assistant entity. Takes an entityId and an optional @@ -34,7 +34,7 @@ func (ha *HomeAssistant) Toggle(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - ha.conn.WriteMessage(req, ha.ctx) + ha.conn.WriteMessage(ha.ctx, req) } func (ha *HomeAssistant) TurnOff(entityId string) { @@ -42,5 +42,5 @@ func (ha *HomeAssistant) TurnOff(entityId string) { req.Domain = "homeassistant" req.Service = "turn_off" - ha.conn.WriteMessage(req, ha.ctx) + ha.conn.WriteMessage(ha.ctx, req) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index ac589f7..b1f298b 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -20,7 +20,7 @@ func (ib InputBoolean) TurnOn(entityId string) { req.Domain = "input_boolean" req.Service = "turn_on" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputBoolean) Toggle(entityId string) { @@ -28,19 +28,19 @@ func (ib InputBoolean) Toggle(entityId string) { req.Domain = "input_boolean" req.Service = "toggle" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputBoolean) TurnOff(entityId string) { req := NewBaseServiceRequest(entityId) req.Domain = "input_boolean" req.Service = "turn_off" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputBoolean) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_boolean" req.Service = "reload" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index e0ec541..488ad94 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -20,12 +20,12 @@ func (ib InputButton) Press(entityId string) { req.Domain = "input_button" req.Service = "press" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputButton) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_button" req.Service = "reload" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 92c12d5..53512a1 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -25,12 +25,12 @@ func (ib InputDatetime) Set(entityId string, value time.Time) { "timestamp": fmt.Sprint(value.Unix()), } - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputDatetime) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_datetime" req.Service = "reload" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 59409f6..2d2646a 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -21,7 +21,7 @@ func (ib InputNumber) Set(entityId string, value float32) { req.Service = "set_value" req.ServiceData = map[string]any{"value": value} - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputNumber) Increment(entityId string) { @@ -29,7 +29,7 @@ func (ib InputNumber) Increment(entityId string) { req.Domain = "input_number" req.Service = "increment" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputNumber) Decrement(entityId string) { @@ -37,12 +37,12 @@ func (ib InputNumber) Decrement(entityId string) { req.Domain = "input_number" req.Service = "decrement" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputNumber) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_number" req.Service = "reload" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index b7a0d1a..cd5d41e 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -23,12 +23,12 @@ func (ib InputText) Set(entityId string, value string) { "value": value, } - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } func (ib InputText) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_text" req.Service = "reload" - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } diff --git a/internal/services/light.go b/internal/services/light.go index c1a2179..5469f99 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -25,7 +25,7 @@ func (l Light) TurnOn(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(req, l.ctx) + l.conn.WriteMessage(l.ctx, req) } // Toggle a light entity. Takes an entityId and an optional @@ -38,12 +38,12 @@ func (l Light) Toggle(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(req, l.ctx) + l.conn.WriteMessage(l.ctx, req) } func (l Light) TurnOff(entityId string) { req := NewBaseServiceRequest(entityId) req.Domain = "light" req.Service = "turn_off" - l.conn.WriteMessage(req, l.ctx) + l.conn.WriteMessage(l.ctx, req) } diff --git a/internal/services/lock.go b/internal/services/lock.go index e122b25..4d5f7f9 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -25,7 +25,7 @@ func (l Lock) Lock(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(req, l.ctx) + l.conn.WriteMessage(l.ctx, req) } // Unlock a lock entity. Takes an entityId and an optional @@ -38,5 +38,5 @@ func (l Lock) Unlock(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(req, l.ctx) + l.conn.WriteMessage(l.ctx, req) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 727d7a9..61c5ab0 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -22,7 +22,7 @@ func (mp MediaPlayer) ClearPlaylist(entityId string) { req.Domain = "media_player" req.Service = "clear_playlist" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Group players together. Only works on platforms with support for player groups. @@ -36,7 +36,7 @@ func (mp MediaPlayer) Join(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the command for next track. @@ -46,7 +46,7 @@ func (mp MediaPlayer) Next(entityId string) { req.Domain = "media_player" req.Service = "media_next_track" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the command for pause. @@ -56,7 +56,7 @@ func (mp MediaPlayer) Pause(entityId string) { req.Domain = "media_player" req.Service = "media_pause" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the command for play. @@ -66,7 +66,7 @@ func (mp MediaPlayer) Play(entityId string) { req.Domain = "media_player" req.Service = "media_play" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Toggle media player play/pause state. @@ -76,7 +76,7 @@ func (mp MediaPlayer) PlayPause(entityId string) { req.Domain = "media_player" req.Service = "media_play_pause" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the command for previous track. @@ -86,7 +86,7 @@ func (mp MediaPlayer) Previous(entityId string) { req.Domain = "media_player" req.Service = "media_previous_track" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the command to seek in current playing media. @@ -100,7 +100,7 @@ func (mp MediaPlayer) Seek(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the stop command. @@ -110,7 +110,7 @@ func (mp MediaPlayer) Stop(entityId string) { req.Domain = "media_player" req.Service = "media_stop" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the command for playing media. @@ -124,7 +124,7 @@ func (mp MediaPlayer) PlayMedia(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Set repeat mode. Takes an entityId and an optional @@ -137,7 +137,7 @@ func (mp MediaPlayer) RepeatSet(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the command to change sound mode. @@ -151,7 +151,7 @@ func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData ...map[string req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Send the media player the command to change input source. @@ -165,7 +165,7 @@ func (mp MediaPlayer) SelectSource(entityId string, serviceData ...map[string]an req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Set shuffling state. @@ -179,7 +179,7 @@ func (mp MediaPlayer) Shuffle(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Toggles a media player power state. @@ -189,7 +189,7 @@ func (mp MediaPlayer) Toggle(entityId string) { req.Domain = "media_player" req.Service = "toggle" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Turn a media player power off. @@ -199,7 +199,7 @@ func (mp MediaPlayer) TurnOff(entityId string) { req.Domain = "media_player" req.Service = "turn_off" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Turn a media player power on. @@ -209,7 +209,7 @@ func (mp MediaPlayer) TurnOn(entityId string) { req.Domain = "media_player" req.Service = "turn_on" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Unjoin the player from a group. Only works on @@ -220,7 +220,7 @@ func (mp MediaPlayer) Unjoin(entityId string) { req.Domain = "media_player" req.Service = "unjoin" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Turn a media player volume down. @@ -230,7 +230,7 @@ func (mp MediaPlayer) VolumeDown(entityId string) { req.Domain = "media_player" req.Service = "volume_down" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Mute a media player's volume. @@ -244,7 +244,7 @@ func (mp MediaPlayer) VolumeMute(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Set a media player's volume level. @@ -258,7 +258,7 @@ func (mp MediaPlayer) VolumeSet(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } // Turn a media player volume up. @@ -268,5 +268,5 @@ func (mp MediaPlayer) VolumeUp(entityId string) { req.Domain = "media_player" req.Service = "volume_up" - mp.conn.WriteMessage(req, mp.ctx) + mp.conn.WriteMessage(mp.ctx, req) } diff --git a/internal/services/notify.go b/internal/services/notify.go index e76dd42..228a7bd 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -26,5 +26,5 @@ func (ha *Notify) Notify(reqData types.NotifyRequest) { } req.ServiceData = serviceData - ha.conn.WriteMessage(req, ha.ctx) + ha.conn.WriteMessage(ha.ctx, req) } diff --git a/internal/services/number.go b/internal/services/number.go index 243603e..e8f0a61 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -21,5 +21,5 @@ func (ib Number) SetValue(entityId string, value float32) { req.Service = "set_value" req.ServiceData = map[string]any{"value": value} - ib.conn.WriteMessage(req, ib.ctx) + ib.conn.WriteMessage(ib.ctx, req) } diff --git a/internal/services/scene.go b/internal/services/scene.go index e17ada9..5d17aba 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -24,7 +24,7 @@ func (s Scene) Apply(serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } // Create a scene entity. Takes an entityId and an optional @@ -37,7 +37,7 @@ func (s Scene) Create(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } // Reload the scenes. @@ -46,7 +46,7 @@ func (s Scene) Reload() { req.Domain = "scene" req.Service = "reload" - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } // TurnOn a scene entity. Takes an entityId and an optional @@ -59,5 +59,5 @@ func (s Scene) TurnOn(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } diff --git a/internal/services/script.go b/internal/services/script.go index b80dbbb..6f4d478 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -21,7 +21,7 @@ func (s Script) Reload(entityId string) { req.Domain = "script" req.Service = "reload" - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } // Toggle a script that was created in the HA UI. @@ -30,7 +30,7 @@ func (s Script) Toggle(entityId string) { req.Domain = "script" req.Service = "toggle" - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } // Turn off a script that was created in the HA UI. @@ -39,7 +39,7 @@ func (s Script) TurnOff() { req.Domain = "script" req.Service = "turn_off" - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } // Turn on a script that was created in the HA UI. @@ -48,5 +48,5 @@ func (s Script) TurnOn(entityId string) { req.Domain = "script" req.Service = "turn_on" - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } diff --git a/internal/services/services.go b/internal/services/services.go index 6dbb024..1348c47 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -29,7 +29,7 @@ func BuildService[ TTS | Vacuum | ZWaveJS, -](conn *ws.WebsocketWriter, ctx context.Context) *T { +](ctx context.Context, conn *ws.WebsocketWriter) *T { return &T{conn: conn, ctx: ctx} } diff --git a/internal/services/switch.go b/internal/services/switch.go index 0e7be52..060662f 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -20,7 +20,7 @@ func (s Switch) TurnOn(entityId string) { req.Domain = "switch" req.Service = "turn_on" - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } func (s Switch) Toggle(entityId string) { @@ -28,12 +28,12 @@ func (s Switch) Toggle(entityId string) { req.Domain = "switch" req.Service = "toggle" - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } func (s Switch) TurnOff(entityId string) { req := NewBaseServiceRequest(entityId) req.Domain = "switch" req.Service = "turn_off" - s.conn.WriteMessage(req, s.ctx) + s.conn.WriteMessage(s.ctx, req) } diff --git a/internal/services/tts.go b/internal/services/tts.go index 74b4963..a0efe42 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -21,7 +21,7 @@ func (tts TTS) ClearCache() { req.Domain = "tts" req.Service = "clear_cache" - tts.conn.WriteMessage(req, tts.ctx) + tts.conn.WriteMessage(tts.ctx, req) } // Say something using text-to-speech on a media player with cloud. @@ -35,7 +35,7 @@ func (tts TTS) CloudSay(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - tts.conn.WriteMessage(req, tts.ctx) + tts.conn.WriteMessage(tts.ctx, req) } // Say something using text-to-speech on a media player with google_translate. @@ -49,5 +49,5 @@ func (tts TTS) GoogleTranslateSay(entityId string, serviceData ...map[string]any req.ServiceData = serviceData[0] } - tts.conn.WriteMessage(req, tts.ctx) + tts.conn.WriteMessage(tts.ctx, req) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index fbc71b0..c53bd44 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -22,7 +22,7 @@ func (v Vacuum) CleanSpot(entityId string) { req.Domain = "vacuum" req.Service = "clean_spot" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Locate the vacuum cleaner robot. @@ -32,7 +32,7 @@ func (v Vacuum) Locate(entityId string) { req.Domain = "vacuum" req.Service = "locate" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Pause the cleaning task. @@ -42,7 +42,7 @@ func (v Vacuum) Pause(entityId string) { req.Domain = "vacuum" req.Service = "pause" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Tell the vacuum cleaner to return to its dock. @@ -52,7 +52,7 @@ func (v Vacuum) ReturnToBase(entityId string) { req.Domain = "vacuum" req.Service = "return_to_base" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Send a raw command to the vacuum cleaner. Takes an entityId and an optional @@ -65,7 +65,7 @@ func (v Vacuum) SendCommand(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Set the fan speed of the vacuum cleaner. Takes an entityId and an optional @@ -79,7 +79,7 @@ func (v Vacuum) SetFanSpeed(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Start or resume the cleaning task. @@ -89,7 +89,7 @@ func (v Vacuum) Start(entityId string) { req.Domain = "vacuum" req.Service = "start" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Start, pause, or resume the cleaning task. @@ -99,7 +99,7 @@ func (v Vacuum) StartPause(entityId string) { req.Domain = "vacuum" req.Service = "start_pause" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Stop the current cleaning task. @@ -109,7 +109,7 @@ func (v Vacuum) Stop(entityId string) { req.Domain = "vacuum" req.Service = "stop" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Stop the current cleaning task and return to home. @@ -119,7 +119,7 @@ func (v Vacuum) TurnOff(entityId string) { req.Domain = "vacuum" req.Service = "turn_off" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } // Start a new cleaning task. @@ -129,5 +129,5 @@ func (v Vacuum) TurnOn(entityId string) { req.Domain = "vacuum" req.Service = "turn_on" - v.conn.WriteMessage(req, v.ctx) + v.conn.WriteMessage(v.ctx, req) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index f19fc6f..9bca121 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -25,5 +25,5 @@ func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, valu "value": value, } - zw.conn.WriteMessage(req, zw.ctx) + zw.conn.WriteMessage(zw.ctx, req) } diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index 6ac4640..fd9ca54 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -21,9 +21,9 @@ type ChanMsg struct { Raw []byte } -func ListenWebsocket(conn *websocket.Conn, ctx context.Context, c chan ChanMsg) { +func ListenWebsocket(ctx context.Context, conn *websocket.Conn, c chan ChanMsg) { for { - bytes, err := ReadMessage(conn, ctx) + bytes, err := ReadMessage(ctx, conn) if err != nil { slog.Error("Error reading from websocket:", err) close(c) diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 2eec28b..1b896f3 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -29,7 +29,7 @@ type WebsocketWriter struct { mutex sync.Mutex } -func (w *WebsocketWriter) WriteMessage(msg interface{}, ctx context.Context) error { +func (w *WebsocketWriter) WriteMessage(ctx context.Context, msg interface{}) error { w.mutex.Lock() defer w.mutex.Unlock() @@ -41,7 +41,7 @@ func (w *WebsocketWriter) WriteMessage(msg interface{}, ctx context.Context) err return nil } -func ReadMessage(conn *websocket.Conn, ctx context.Context) ([]byte, error) { +func ReadMessage(ctx context.Context, conn *websocket.Conn) ([]byte, error) { _, msg, err := conn.ReadMessage() if err != nil { return []byte{}, err @@ -72,7 +72,7 @@ func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, } // Read auth_required message - _, err = ReadMessage(conn, ctx) + _, err = ReadMessage(ctx, conn) if err != nil { ctxCancel() slog.Error("Unknown error creating websocket client\n") @@ -80,7 +80,7 @@ func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, } // Send auth message - err = SendAuthMessage(conn, ctx, authToken) + err = SendAuthMessage(ctx, conn, authToken) if err != nil { ctxCancel() slog.Error("Unknown error creating websocket client\n") @@ -88,7 +88,7 @@ func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, } // Verify auth message was successful - err = VerifyAuthResponse(conn, ctx) + err = VerifyAuthResponse(ctx, conn) if err != nil { ctxCancel() slog.Error("Auth token is invalid. Please double check it or create a new token in your Home Assistant profile\n") @@ -98,7 +98,7 @@ func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, return conn, ctx, ctxCancel, nil } -func SendAuthMessage(conn *websocket.Conn, ctx context.Context, token string) error { +func SendAuthMessage(ctx context.Context, conn *websocket.Conn, token string) error { err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token}) if err != nil { return err @@ -111,8 +111,8 @@ type authResponse struct { Message string `json:"message"` } -func VerifyAuthResponse(conn *websocket.Conn, ctx context.Context) error { - msg, err := ReadMessage(conn, ctx) +func VerifyAuthResponse(ctx context.Context, conn *websocket.Conn) error { + msg, err := ReadMessage(ctx, conn) if err != nil { return err } @@ -133,11 +133,11 @@ type SubEvent struct { EventType string `json:"event_type"` } -func SubscribeToStateChangedEvents(id int64, conn *WebsocketWriter, ctx context.Context) { - SubscribeToEventType("state_changed", conn, ctx, id) +func SubscribeToStateChangedEvents(ctx context.Context, id int64, conn *WebsocketWriter) { + SubscribeToEventType(ctx, "state_changed", conn, id) } -func SubscribeToEventType(eventType string, conn *WebsocketWriter, ctx context.Context, id ...int64) { +func SubscribeToEventType(ctx context.Context, eventType string, conn *WebsocketWriter, id ...int64) { var finalId int64 if len(id) == 0 { finalId = i.GetId() @@ -149,7 +149,7 @@ func SubscribeToEventType(eventType string, conn *WebsocketWriter, ctx context.C Type: "subscribe_events", EventType: eventType, } - err := conn.WriteMessage(e, ctx) + err := conn.WriteMessage(ctx, e) if err != nil { wrappedErr := fmt.Errorf("error writing to websocket: %w", err) slog.Error(wrappedErr.Error()) diff --git a/service.go b/service.go index c861ec7..f45be36 100644 --- a/service.go +++ b/service.go @@ -32,28 +32,28 @@ type Service struct { ZWaveJS *services.ZWaveJS } -func newService(conn *ws.WebsocketWriter, ctx context.Context, httpClient *http.HttpClient) *Service { +func newService(ctx context.Context, conn *ws.WebsocketWriter, httpClient *http.HttpClient) *Service { return &Service{ - AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn, ctx), - Climate: services.BuildService[services.Climate](conn, ctx), - Cover: services.BuildService[services.Cover](conn, ctx), - Light: services.BuildService[services.Light](conn, ctx), - HomeAssistant: services.BuildService[services.HomeAssistant](conn, ctx), - Lock: services.BuildService[services.Lock](conn, ctx), - MediaPlayer: services.BuildService[services.MediaPlayer](conn, ctx), - Switch: services.BuildService[services.Switch](conn, ctx), - InputBoolean: services.BuildService[services.InputBoolean](conn, ctx), - InputButton: services.BuildService[services.InputButton](conn, ctx), - InputText: services.BuildService[services.InputText](conn, ctx), - InputDatetime: services.BuildService[services.InputDatetime](conn, ctx), - InputNumber: services.BuildService[services.InputNumber](conn, ctx), - Event: services.BuildService[services.Event](conn, ctx), - Notify: services.BuildService[services.Notify](conn, ctx), - Number: services.BuildService[services.Number](conn, ctx), - Scene: services.BuildService[services.Scene](conn, ctx), - Script: services.BuildService[services.Script](conn, ctx), - TTS: services.BuildService[services.TTS](conn, ctx), - Vacuum: services.BuildService[services.Vacuum](conn, ctx), - ZWaveJS: services.BuildService[services.ZWaveJS](conn, ctx), + AlarmControlPanel: services.BuildService[services.AlarmControlPanel](ctx, conn), + Climate: services.BuildService[services.Climate](ctx, conn), + Cover: services.BuildService[services.Cover](ctx, conn), + Light: services.BuildService[services.Light](ctx, conn), + HomeAssistant: services.BuildService[services.HomeAssistant](ctx, conn), + Lock: services.BuildService[services.Lock](ctx, conn), + MediaPlayer: services.BuildService[services.MediaPlayer](ctx, conn), + Switch: services.BuildService[services.Switch](ctx, conn), + InputBoolean: services.BuildService[services.InputBoolean](ctx, conn), + InputButton: services.BuildService[services.InputButton](ctx, conn), + InputText: services.BuildService[services.InputText](ctx, conn), + InputDatetime: services.BuildService[services.InputDatetime](ctx, conn), + InputNumber: services.BuildService[services.InputNumber](ctx, conn), + Event: services.BuildService[services.Event](ctx, conn), + Notify: services.BuildService[services.Notify](ctx, conn), + Number: services.BuildService[services.Number](ctx, conn), + Scene: services.BuildService[services.Scene](ctx, conn), + Script: services.BuildService[services.Script](ctx, conn), + TTS: services.BuildService[services.TTS](ctx, conn), + Vacuum: services.BuildService[services.Vacuum](ctx, conn), + ZWaveJS: services.BuildService[services.ZWaveJS](ctx, conn), } } From f56fee746f0314b89ff37060d7ebc10533327239 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 15:08:54 +0100 Subject: [PATCH 003/103] Remove some unused `ctx` arguments Remove the `ctx` arguments from the following functions. The arguments are not used, and there doesn't seem to be much prospect for making these functions cancelable: * `websocket.WriteMessage()` * `websocket.ReadMessage()` * `websocket.SendAuthMessage()` * `websocket.VerifyAuthResponse()` * `websocket.SubscribeToStateChangedEvents()` * `websocket.SubscribeToEventType()` * `websocket.ListenWebsocket()` --- app.go | 6 ++-- internal/services/alarm_control_panel.go | 14 ++++---- internal/services/climate.go | 4 +-- internal/services/cover.go | 20 +++++------ internal/services/event.go | 2 +- internal/services/homeassistant.go | 6 ++-- internal/services/input_boolean.go | 8 ++--- internal/services/input_button.go | 4 +-- internal/services/input_datetime.go | 4 +-- internal/services/input_number.go | 8 ++--- internal/services/input_text.go | 4 +-- internal/services/light.go | 6 ++-- internal/services/lock.go | 4 +-- internal/services/media_player.go | 44 ++++++++++++------------ internal/services/notify.go | 2 +- internal/services/number.go | 2 +- internal/services/scene.go | 8 ++--- internal/services/script.go | 8 ++--- internal/services/switch.go | 6 ++-- internal/services/tts.go | 6 ++-- internal/services/vacuum.go | 22 ++++++------ internal/services/zwavejs.go | 2 +- internal/websocket/reader.go | 5 ++- internal/websocket/websocket.go | 24 ++++++------- 24 files changed, 109 insertions(+), 110 deletions(-) diff --git a/app.go b/app.go index 8dc492d..833e424 100644 --- a/app.go +++ b/app.go @@ -256,7 +256,7 @@ func (a *App) RegisterEventListeners(evls ...EventListener) { if elList, ok := a.eventListeners[eventType]; ok { a.eventListeners[eventType] = append(elList, &evl) } else { - ws.SubscribeToEventType(a.ctx, eventType, a.wsWriter) + ws.SubscribeToEventType(eventType, a.wsWriter) a.eventListeners[eventType] = []*EventListener{&evl} } } @@ -316,7 +316,7 @@ func (a *App) Start() { // subscribe to state_changed events id := internal.GetId() - ws.SubscribeToStateChangedEvents(a.ctx, id, a.wsWriter) + ws.SubscribeToStateChangedEvents(id, a.wsWriter) a.entityListenersId = id // entity listeners runOnStartup @@ -345,7 +345,7 @@ func (a *App) Start() { // entity listeners and event listeners elChan := make(chan ws.ChanMsg) - go ws.ListenWebsocket(a.ctx, a.conn, elChan) + go ws.ListenWebsocket(a.conn, elChan) for { msg, ok := <-elChan diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 38e5c0d..721a0c5 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -26,7 +26,7 @@ func (acp AlarmControlPanel) ArmAway(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(acp.ctx, req) + acp.conn.WriteMessage(req) } // Send the alarm the command for arm away. @@ -40,7 +40,7 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData .. req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(acp.ctx, req) + acp.conn.WriteMessage(req) } // Send the alarm the command for arm home. @@ -54,7 +54,7 @@ func (acp AlarmControlPanel) ArmHome(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(acp.ctx, req) + acp.conn.WriteMessage(req) } // Send the alarm the command for arm night. @@ -68,7 +68,7 @@ func (acp AlarmControlPanel) ArmNight(entityId string, serviceData ...map[string req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(acp.ctx, req) + acp.conn.WriteMessage(req) } // Send the alarm the command for arm vacation. @@ -82,7 +82,7 @@ func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData ...map[str req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(acp.ctx, req) + acp.conn.WriteMessage(req) } // Send the alarm the command for disarm. @@ -96,7 +96,7 @@ func (acp AlarmControlPanel) Disarm(entityId string, serviceData ...map[string]a req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(acp.ctx, req) + acp.conn.WriteMessage(req) } // Send the alarm the command for trigger. @@ -110,5 +110,5 @@ func (acp AlarmControlPanel) Trigger(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(acp.ctx, req) + acp.conn.WriteMessage(req) } diff --git a/internal/services/climate.go b/internal/services/climate.go index d37460f..cb4cfa3 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -22,7 +22,7 @@ func (c Climate) SetFanMode(entityId string, fanMode string) { req.Service = "set_fan_mode" req.ServiceData = map[string]any{"fan_mode": fanMode} - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatureRequest) { @@ -31,5 +31,5 @@ func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatur req.Service = "set_temperature" req.ServiceData = serviceData.ToJSON() - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } diff --git a/internal/services/cover.go b/internal/services/cover.go index 83b0d83..84fcd9a 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -21,7 +21,7 @@ func (c Cover) Close(entityId string) { req.Domain = "cover" req.Service = "close_cover" - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Close all or specified cover tilt. Takes an entityId. @@ -30,7 +30,7 @@ func (c Cover) CloseTilt(entityId string) { req.Domain = "cover" req.Service = "close_cover_tilt" - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Open all or specified cover. Takes an entityId. @@ -39,7 +39,7 @@ func (c Cover) Open(entityId string) { req.Domain = "cover" req.Service = "open_cover" - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Open all or specified cover tilt. Takes an entityId. @@ -48,7 +48,7 @@ func (c Cover) OpenTilt(entityId string) { req.Domain = "cover" req.Service = "open_cover_tilt" - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Move to specific position all or specified cover. Takes an entityId and an optional @@ -61,7 +61,7 @@ func (c Cover) SetPosition(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Move to specific position all or specified cover tilt. Takes an entityId and an optional @@ -74,7 +74,7 @@ func (c Cover) SetTiltPosition(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Stop a cover entity. Takes an entityId. @@ -83,7 +83,7 @@ func (c Cover) Stop(entityId string) { req.Domain = "cover" req.Service = "stop_cover" - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Stop a cover entity tilt. Takes an entityId. @@ -92,7 +92,7 @@ func (c Cover) StopTilt(entityId string) { req.Domain = "cover" req.Service = "stop_cover_tilt" - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Toggle a cover open/closed. Takes an entityId. @@ -101,7 +101,7 @@ func (c Cover) Toggle(entityId string) { req.Domain = "cover" req.Service = "toggle" - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } // Toggle a cover tilt open/closed. Takes an entityId. @@ -110,5 +110,5 @@ func (c Cover) ToggleTilt(entityId string) { req.Domain = "cover" req.Service = "toggle_cover_tilt" - c.conn.WriteMessage(c.ctx, req) + c.conn.WriteMessage(req) } diff --git a/internal/services/event.go b/internal/services/event.go index d347cd0..587a0ca 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -35,5 +35,5 @@ func (e Event) Fire(eventType string, eventData ...map[string]any) { req.EventData = eventData[0] } - e.conn.WriteMessage(e.ctx, req) + e.conn.WriteMessage(req) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index f7b499d..88d7046 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -21,7 +21,7 @@ func (ha *HomeAssistant) TurnOn(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - ha.conn.WriteMessage(ha.ctx, req) + ha.conn.WriteMessage(req) } // Toggle a Home Assistant entity. Takes an entityId and an optional @@ -34,7 +34,7 @@ func (ha *HomeAssistant) Toggle(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - ha.conn.WriteMessage(ha.ctx, req) + ha.conn.WriteMessage(req) } func (ha *HomeAssistant) TurnOff(entityId string) { @@ -42,5 +42,5 @@ func (ha *HomeAssistant) TurnOff(entityId string) { req.Domain = "homeassistant" req.Service = "turn_off" - ha.conn.WriteMessage(ha.ctx, req) + ha.conn.WriteMessage(req) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index b1f298b..08ee10b 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -20,7 +20,7 @@ func (ib InputBoolean) TurnOn(entityId string) { req.Domain = "input_boolean" req.Service = "turn_on" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputBoolean) Toggle(entityId string) { @@ -28,19 +28,19 @@ func (ib InputBoolean) Toggle(entityId string) { req.Domain = "input_boolean" req.Service = "toggle" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputBoolean) TurnOff(entityId string) { req := NewBaseServiceRequest(entityId) req.Domain = "input_boolean" req.Service = "turn_off" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputBoolean) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_boolean" req.Service = "reload" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 488ad94..57f1865 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -20,12 +20,12 @@ func (ib InputButton) Press(entityId string) { req.Domain = "input_button" req.Service = "press" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputButton) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_button" req.Service = "reload" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 53512a1..555a2ce 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -25,12 +25,12 @@ func (ib InputDatetime) Set(entityId string, value time.Time) { "timestamp": fmt.Sprint(value.Unix()), } - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputDatetime) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_datetime" req.Service = "reload" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 2d2646a..192030d 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -21,7 +21,7 @@ func (ib InputNumber) Set(entityId string, value float32) { req.Service = "set_value" req.ServiceData = map[string]any{"value": value} - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputNumber) Increment(entityId string) { @@ -29,7 +29,7 @@ func (ib InputNumber) Increment(entityId string) { req.Domain = "input_number" req.Service = "increment" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputNumber) Decrement(entityId string) { @@ -37,12 +37,12 @@ func (ib InputNumber) Decrement(entityId string) { req.Domain = "input_number" req.Service = "decrement" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputNumber) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_number" req.Service = "reload" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index cd5d41e..eb5b1a6 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -23,12 +23,12 @@ func (ib InputText) Set(entityId string, value string) { "value": value, } - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } func (ib InputText) Reload() { req := NewBaseServiceRequest("") req.Domain = "input_text" req.Service = "reload" - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } diff --git a/internal/services/light.go b/internal/services/light.go index 5469f99..7f5bac4 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -25,7 +25,7 @@ func (l Light) TurnOn(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(l.ctx, req) + l.conn.WriteMessage(req) } // Toggle a light entity. Takes an entityId and an optional @@ -38,12 +38,12 @@ func (l Light) Toggle(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(l.ctx, req) + l.conn.WriteMessage(req) } func (l Light) TurnOff(entityId string) { req := NewBaseServiceRequest(entityId) req.Domain = "light" req.Service = "turn_off" - l.conn.WriteMessage(l.ctx, req) + l.conn.WriteMessage(req) } diff --git a/internal/services/lock.go b/internal/services/lock.go index 4d5f7f9..34f6335 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -25,7 +25,7 @@ func (l Lock) Lock(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(l.ctx, req) + l.conn.WriteMessage(req) } // Unlock a lock entity. Takes an entityId and an optional @@ -38,5 +38,5 @@ func (l Lock) Unlock(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(l.ctx, req) + l.conn.WriteMessage(req) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 61c5ab0..13762e1 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -22,7 +22,7 @@ func (mp MediaPlayer) ClearPlaylist(entityId string) { req.Domain = "media_player" req.Service = "clear_playlist" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Group players together. Only works on platforms with support for player groups. @@ -36,7 +36,7 @@ func (mp MediaPlayer) Join(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the command for next track. @@ -46,7 +46,7 @@ func (mp MediaPlayer) Next(entityId string) { req.Domain = "media_player" req.Service = "media_next_track" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the command for pause. @@ -56,7 +56,7 @@ func (mp MediaPlayer) Pause(entityId string) { req.Domain = "media_player" req.Service = "media_pause" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the command for play. @@ -66,7 +66,7 @@ func (mp MediaPlayer) Play(entityId string) { req.Domain = "media_player" req.Service = "media_play" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Toggle media player play/pause state. @@ -76,7 +76,7 @@ func (mp MediaPlayer) PlayPause(entityId string) { req.Domain = "media_player" req.Service = "media_play_pause" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the command for previous track. @@ -86,7 +86,7 @@ func (mp MediaPlayer) Previous(entityId string) { req.Domain = "media_player" req.Service = "media_previous_track" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the command to seek in current playing media. @@ -100,7 +100,7 @@ func (mp MediaPlayer) Seek(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the stop command. @@ -110,7 +110,7 @@ func (mp MediaPlayer) Stop(entityId string) { req.Domain = "media_player" req.Service = "media_stop" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the command for playing media. @@ -124,7 +124,7 @@ func (mp MediaPlayer) PlayMedia(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Set repeat mode. Takes an entityId and an optional @@ -137,7 +137,7 @@ func (mp MediaPlayer) RepeatSet(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the command to change sound mode. @@ -151,7 +151,7 @@ func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData ...map[string req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Send the media player the command to change input source. @@ -165,7 +165,7 @@ func (mp MediaPlayer) SelectSource(entityId string, serviceData ...map[string]an req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Set shuffling state. @@ -179,7 +179,7 @@ func (mp MediaPlayer) Shuffle(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Toggles a media player power state. @@ -189,7 +189,7 @@ func (mp MediaPlayer) Toggle(entityId string) { req.Domain = "media_player" req.Service = "toggle" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Turn a media player power off. @@ -199,7 +199,7 @@ func (mp MediaPlayer) TurnOff(entityId string) { req.Domain = "media_player" req.Service = "turn_off" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Turn a media player power on. @@ -209,7 +209,7 @@ func (mp MediaPlayer) TurnOn(entityId string) { req.Domain = "media_player" req.Service = "turn_on" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Unjoin the player from a group. Only works on @@ -220,7 +220,7 @@ func (mp MediaPlayer) Unjoin(entityId string) { req.Domain = "media_player" req.Service = "unjoin" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Turn a media player volume down. @@ -230,7 +230,7 @@ func (mp MediaPlayer) VolumeDown(entityId string) { req.Domain = "media_player" req.Service = "volume_down" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Mute a media player's volume. @@ -244,7 +244,7 @@ func (mp MediaPlayer) VolumeMute(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Set a media player's volume level. @@ -258,7 +258,7 @@ func (mp MediaPlayer) VolumeSet(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } // Turn a media player volume up. @@ -268,5 +268,5 @@ func (mp MediaPlayer) VolumeUp(entityId string) { req.Domain = "media_player" req.Service = "volume_up" - mp.conn.WriteMessage(mp.ctx, req) + mp.conn.WriteMessage(req) } diff --git a/internal/services/notify.go b/internal/services/notify.go index 228a7bd..f3b4c6b 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -26,5 +26,5 @@ func (ha *Notify) Notify(reqData types.NotifyRequest) { } req.ServiceData = serviceData - ha.conn.WriteMessage(ha.ctx, req) + ha.conn.WriteMessage(req) } diff --git a/internal/services/number.go b/internal/services/number.go index e8f0a61..2f9b7ed 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -21,5 +21,5 @@ func (ib Number) SetValue(entityId string, value float32) { req.Service = "set_value" req.ServiceData = map[string]any{"value": value} - ib.conn.WriteMessage(ib.ctx, req) + ib.conn.WriteMessage(req) } diff --git a/internal/services/scene.go b/internal/services/scene.go index 5d17aba..a6330df 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -24,7 +24,7 @@ func (s Scene) Apply(serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } // Create a scene entity. Takes an entityId and an optional @@ -37,7 +37,7 @@ func (s Scene) Create(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } // Reload the scenes. @@ -46,7 +46,7 @@ func (s Scene) Reload() { req.Domain = "scene" req.Service = "reload" - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } // TurnOn a scene entity. Takes an entityId and an optional @@ -59,5 +59,5 @@ func (s Scene) TurnOn(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } diff --git a/internal/services/script.go b/internal/services/script.go index 6f4d478..fb191f8 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -21,7 +21,7 @@ func (s Script) Reload(entityId string) { req.Domain = "script" req.Service = "reload" - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } // Toggle a script that was created in the HA UI. @@ -30,7 +30,7 @@ func (s Script) Toggle(entityId string) { req.Domain = "script" req.Service = "toggle" - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } // Turn off a script that was created in the HA UI. @@ -39,7 +39,7 @@ func (s Script) TurnOff() { req.Domain = "script" req.Service = "turn_off" - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } // Turn on a script that was created in the HA UI. @@ -48,5 +48,5 @@ func (s Script) TurnOn(entityId string) { req.Domain = "script" req.Service = "turn_on" - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } diff --git a/internal/services/switch.go b/internal/services/switch.go index 060662f..0f602c5 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -20,7 +20,7 @@ func (s Switch) TurnOn(entityId string) { req.Domain = "switch" req.Service = "turn_on" - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } func (s Switch) Toggle(entityId string) { @@ -28,12 +28,12 @@ func (s Switch) Toggle(entityId string) { req.Domain = "switch" req.Service = "toggle" - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } func (s Switch) TurnOff(entityId string) { req := NewBaseServiceRequest(entityId) req.Domain = "switch" req.Service = "turn_off" - s.conn.WriteMessage(s.ctx, req) + s.conn.WriteMessage(req) } diff --git a/internal/services/tts.go b/internal/services/tts.go index a0efe42..ab65d45 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -21,7 +21,7 @@ func (tts TTS) ClearCache() { req.Domain = "tts" req.Service = "clear_cache" - tts.conn.WriteMessage(tts.ctx, req) + tts.conn.WriteMessage(req) } // Say something using text-to-speech on a media player with cloud. @@ -35,7 +35,7 @@ func (tts TTS) CloudSay(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - tts.conn.WriteMessage(tts.ctx, req) + tts.conn.WriteMessage(req) } // Say something using text-to-speech on a media player with google_translate. @@ -49,5 +49,5 @@ func (tts TTS) GoogleTranslateSay(entityId string, serviceData ...map[string]any req.ServiceData = serviceData[0] } - tts.conn.WriteMessage(tts.ctx, req) + tts.conn.WriteMessage(req) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index c53bd44..fe0d51b 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -22,7 +22,7 @@ func (v Vacuum) CleanSpot(entityId string) { req.Domain = "vacuum" req.Service = "clean_spot" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Locate the vacuum cleaner robot. @@ -32,7 +32,7 @@ func (v Vacuum) Locate(entityId string) { req.Domain = "vacuum" req.Service = "locate" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Pause the cleaning task. @@ -42,7 +42,7 @@ func (v Vacuum) Pause(entityId string) { req.Domain = "vacuum" req.Service = "pause" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Tell the vacuum cleaner to return to its dock. @@ -52,7 +52,7 @@ func (v Vacuum) ReturnToBase(entityId string) { req.Domain = "vacuum" req.Service = "return_to_base" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Send a raw command to the vacuum cleaner. Takes an entityId and an optional @@ -65,7 +65,7 @@ func (v Vacuum) SendCommand(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Set the fan speed of the vacuum cleaner. Takes an entityId and an optional @@ -79,7 +79,7 @@ func (v Vacuum) SetFanSpeed(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Start or resume the cleaning task. @@ -89,7 +89,7 @@ func (v Vacuum) Start(entityId string) { req.Domain = "vacuum" req.Service = "start" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Start, pause, or resume the cleaning task. @@ -99,7 +99,7 @@ func (v Vacuum) StartPause(entityId string) { req.Domain = "vacuum" req.Service = "start_pause" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Stop the current cleaning task. @@ -109,7 +109,7 @@ func (v Vacuum) Stop(entityId string) { req.Domain = "vacuum" req.Service = "stop" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Stop the current cleaning task and return to home. @@ -119,7 +119,7 @@ func (v Vacuum) TurnOff(entityId string) { req.Domain = "vacuum" req.Service = "turn_off" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } // Start a new cleaning task. @@ -129,5 +129,5 @@ func (v Vacuum) TurnOn(entityId string) { req.Domain = "vacuum" req.Service = "turn_on" - v.conn.WriteMessage(v.ctx, req) + v.conn.WriteMessage(req) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 9bca121..eae2677 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -25,5 +25,5 @@ func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, valu "value": value, } - zw.conn.WriteMessage(zw.ctx, req) + zw.conn.WriteMessage(req) } diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index fd9ca54..06704a0 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -1,7 +1,6 @@ package websocket import ( - "context" "encoding/json" "log/slog" @@ -21,9 +20,9 @@ type ChanMsg struct { Raw []byte } -func ListenWebsocket(ctx context.Context, conn *websocket.Conn, c chan ChanMsg) { +func ListenWebsocket(conn *websocket.Conn, c chan ChanMsg) { for { - bytes, err := ReadMessage(ctx, conn) + bytes, err := ReadMessage(conn) if err != nil { slog.Error("Error reading from websocket:", err) close(c) diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 1b896f3..8d7985b 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -29,7 +29,7 @@ type WebsocketWriter struct { mutex sync.Mutex } -func (w *WebsocketWriter) WriteMessage(ctx context.Context, msg interface{}) error { +func (w *WebsocketWriter) WriteMessage(msg interface{}) error { w.mutex.Lock() defer w.mutex.Unlock() @@ -41,7 +41,7 @@ func (w *WebsocketWriter) WriteMessage(ctx context.Context, msg interface{}) err return nil } -func ReadMessage(ctx context.Context, conn *websocket.Conn) ([]byte, error) { +func ReadMessage(conn *websocket.Conn) ([]byte, error) { _, msg, err := conn.ReadMessage() if err != nil { return []byte{}, err @@ -72,7 +72,7 @@ func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, } // Read auth_required message - _, err = ReadMessage(ctx, conn) + _, err = ReadMessage(conn) if err != nil { ctxCancel() slog.Error("Unknown error creating websocket client\n") @@ -80,7 +80,7 @@ func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, } // Send auth message - err = SendAuthMessage(ctx, conn, authToken) + err = SendAuthMessage(conn, authToken) if err != nil { ctxCancel() slog.Error("Unknown error creating websocket client\n") @@ -88,7 +88,7 @@ func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, } // Verify auth message was successful - err = VerifyAuthResponse(ctx, conn) + err = VerifyAuthResponse(conn) if err != nil { ctxCancel() slog.Error("Auth token is invalid. Please double check it or create a new token in your Home Assistant profile\n") @@ -98,7 +98,7 @@ func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, return conn, ctx, ctxCancel, nil } -func SendAuthMessage(ctx context.Context, conn *websocket.Conn, token string) error { +func SendAuthMessage(conn *websocket.Conn, token string) error { err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token}) if err != nil { return err @@ -111,8 +111,8 @@ type authResponse struct { Message string `json:"message"` } -func VerifyAuthResponse(ctx context.Context, conn *websocket.Conn) error { - msg, err := ReadMessage(ctx, conn) +func VerifyAuthResponse(conn *websocket.Conn) error { + msg, err := ReadMessage(conn) if err != nil { return err } @@ -133,11 +133,11 @@ type SubEvent struct { EventType string `json:"event_type"` } -func SubscribeToStateChangedEvents(ctx context.Context, id int64, conn *WebsocketWriter) { - SubscribeToEventType(ctx, "state_changed", conn, id) +func SubscribeToStateChangedEvents(id int64, conn *WebsocketWriter) { + SubscribeToEventType("state_changed", conn, id) } -func SubscribeToEventType(ctx context.Context, eventType string, conn *WebsocketWriter, id ...int64) { +func SubscribeToEventType(eventType string, conn *WebsocketWriter, id ...int64) { var finalId int64 if len(id) == 0 { finalId = i.GetId() @@ -149,7 +149,7 @@ func SubscribeToEventType(ctx context.Context, eventType string, conn *Websocket Type: "subscribe_events", EventType: eventType, } - err := conn.WriteMessage(ctx, e) + err := conn.WriteMessage(e) if err != nil { wrappedErr := fmt.Errorf("error writing to websocket: %w", err) slog.Error(wrappedErr.Error()) From 17fa8bc2d6460755040c643ebd82ff6a44098834 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 15:21:16 +0100 Subject: [PATCH 004/103] Get rid of the `ctx` members in the service subtypes --- internal/services/alarm_control_panel.go | 3 -- internal/services/climate.go | 3 -- internal/services/cover.go | 3 -- internal/services/event.go | 3 -- internal/services/homeassistant.go | 3 -- internal/services/input_boolean.go | 3 -- internal/services/input_button.go | 3 -- internal/services/input_datetime.go | 2 -- internal/services/input_number.go | 3 -- internal/services/input_text.go | 3 -- internal/services/light.go | 3 -- internal/services/lock.go | 3 -- internal/services/media_player.go | 3 -- internal/services/notify.go | 3 -- internal/services/number.go | 3 -- internal/services/scene.go | 3 -- internal/services/script.go | 3 -- internal/services/services.go | 6 ++-- internal/services/switch.go | 3 -- internal/services/tts.go | 3 -- internal/services/vacuum.go | 3 -- internal/services/zwavejs.go | 3 -- service.go | 42 ++++++++++++------------ 23 files changed, 23 insertions(+), 87 deletions(-) diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 721a0c5..3d1605a 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type AlarmControlPanel struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/climate.go b/internal/services/climate.go index cb4cfa3..c29f7ba 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" "saml.dev/gome-assistant/types" ) @@ -11,7 +9,6 @@ import ( type Climate struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/cover.go b/internal/services/cover.go index 84fcd9a..f4f26ed 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type Cover struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/event.go b/internal/services/event.go index 587a0ca..ca9b522 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -1,15 +1,12 @@ package services import ( - "context" - "saml.dev/gome-assistant/internal" ws "saml.dev/gome-assistant/internal/websocket" ) type Event struct { conn *ws.WebsocketWriter - ctx context.Context } // Fire an event diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 88d7046..75376dd 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -1,14 +1,11 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) type HomeAssistant struct { conn *ws.WebsocketWriter - ctx context.Context } // TurnOn a Home Assistant entity. Takes an entityId and an optional diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 08ee10b..89811a9 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type InputBoolean struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 57f1865..8bfeb0a 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type InputButton struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 555a2ce..06d1246 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -1,7 +1,6 @@ package services import ( - "context" "fmt" "time" @@ -12,7 +11,6 @@ import ( type InputDatetime struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 192030d..c82558e 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type InputNumber struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/input_text.go b/internal/services/input_text.go index eb5b1a6..f28dd8c 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type InputText struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/light.go b/internal/services/light.go index 7f5bac4..74b317c 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type Light struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/lock.go b/internal/services/lock.go index 34f6335..fc8b388 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type Lock struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 13762e1..bcf1b14 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type MediaPlayer struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/notify.go b/internal/services/notify.go index f3b4c6b..8ed97da 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -1,15 +1,12 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" "saml.dev/gome-assistant/types" ) type Notify struct { conn *ws.WebsocketWriter - ctx context.Context } // Send a notification. Takes a types.NotifyRequest. diff --git a/internal/services/number.go b/internal/services/number.go index 2f9b7ed..bd04a2a 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type Number struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/scene.go b/internal/services/scene.go index a6330df..87872b8 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type Scene struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/script.go b/internal/services/script.go index fb191f8..793f85b 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type Script struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/services.go b/internal/services/services.go index 1348c47..dab7c57 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -1,8 +1,6 @@ package services import ( - "context" - "saml.dev/gome-assistant/internal" ws "saml.dev/gome-assistant/internal/websocket" ) @@ -29,8 +27,8 @@ func BuildService[ TTS | Vacuum | ZWaveJS, -](ctx context.Context, conn *ws.WebsocketWriter) *T { - return &T{conn: conn, ctx: ctx} +](conn *ws.WebsocketWriter) *T { + return &T{conn: conn} } type BaseServiceRequest struct { diff --git a/internal/services/switch.go b/internal/services/switch.go index 0f602c5..53ec144 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type Switch struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/tts.go b/internal/services/tts.go index ab65d45..e4e5334 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type TTS struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index fe0d51b..f6930cc 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type Vacuum struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index eae2677..8b2fcf9 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -1,8 +1,6 @@ package services import ( - "context" - ws "saml.dev/gome-assistant/internal/websocket" ) @@ -10,7 +8,6 @@ import ( type ZWaveJS struct { conn *ws.WebsocketWriter - ctx context.Context } /* Public API */ diff --git a/service.go b/service.go index f45be36..f09f631 100644 --- a/service.go +++ b/service.go @@ -34,26 +34,26 @@ type Service struct { func newService(ctx context.Context, conn *ws.WebsocketWriter, httpClient *http.HttpClient) *Service { return &Service{ - AlarmControlPanel: services.BuildService[services.AlarmControlPanel](ctx, conn), - Climate: services.BuildService[services.Climate](ctx, conn), - Cover: services.BuildService[services.Cover](ctx, conn), - Light: services.BuildService[services.Light](ctx, conn), - HomeAssistant: services.BuildService[services.HomeAssistant](ctx, conn), - Lock: services.BuildService[services.Lock](ctx, conn), - MediaPlayer: services.BuildService[services.MediaPlayer](ctx, conn), - Switch: services.BuildService[services.Switch](ctx, conn), - InputBoolean: services.BuildService[services.InputBoolean](ctx, conn), - InputButton: services.BuildService[services.InputButton](ctx, conn), - InputText: services.BuildService[services.InputText](ctx, conn), - InputDatetime: services.BuildService[services.InputDatetime](ctx, conn), - InputNumber: services.BuildService[services.InputNumber](ctx, conn), - Event: services.BuildService[services.Event](ctx, conn), - Notify: services.BuildService[services.Notify](ctx, conn), - Number: services.BuildService[services.Number](ctx, conn), - Scene: services.BuildService[services.Scene](ctx, conn), - Script: services.BuildService[services.Script](ctx, conn), - TTS: services.BuildService[services.TTS](ctx, conn), - Vacuum: services.BuildService[services.Vacuum](ctx, conn), - ZWaveJS: services.BuildService[services.ZWaveJS](ctx, 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), + TTS: services.BuildService[services.TTS](conn), + Vacuum: services.BuildService[services.Vacuum](conn), + ZWaveJS: services.BuildService[services.ZWaveJS](conn), } } From 3cb2d8d9a3a758c2948fa3c10266298bc77be084 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 15:23:03 +0100 Subject: [PATCH 005/103] newService(): remove the `ctx` argument --- app.go | 2 +- service.go | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index 833e424..674ef57 100644 --- a/app.go +++ b/app.go @@ -108,7 +108,7 @@ func NewAppFromConfig(config NewAppConfig) (*App, error) { httpClient := http.ClientFromUri(config.RESTBaseURI, config.HAAuthToken) wsWriter := &ws.WebsocketWriter{Conn: conn} - service := newService(ctx, wsWriter, httpClient) + service := newService(wsWriter, httpClient) state, err := newState(httpClient, config.HomeZoneEntityId) if err != nil { return nil, err diff --git a/service.go b/service.go index f09f631..68ce1f1 100644 --- a/service.go +++ b/service.go @@ -1,8 +1,6 @@ package gomeassistant import ( - "context" - "saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/services" ws "saml.dev/gome-assistant/internal/websocket" @@ -32,7 +30,7 @@ type Service struct { ZWaveJS *services.ZWaveJS } -func newService(ctx context.Context, conn *ws.WebsocketWriter, httpClient *http.HttpClient) *Service { +func newService(conn *ws.WebsocketWriter, httpClient *http.HttpClient) *Service { return &Service{ AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn), Climate: services.BuildService[services.Climate](conn), From 77effe98ebf51d5443d118f4477d7d62f7402ab1 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 15:30:19 +0100 Subject: [PATCH 006/103] Add a `ctx` argument to functions that set up a websocket connection Instead of creating and returning a context, change these functions to take a context as argument. --- app.go | 4 +++- internal/websocket/websocket.go | 27 ++++++++++----------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/app.go b/app.go index 674ef57..87478ef 100644 --- a/app.go +++ b/app.go @@ -100,8 +100,10 @@ func NewAppFromConfig(config NewAppConfig) (*App, error) { return nil, ErrInvalidArgs } - conn, ctx, ctxCancel, err := ws.ConnectionFromUri(config.WebsocketURI, config.HAAuthToken) + ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*3) + conn, err := ws.ConnectionFromUri(ctx, config.WebsocketURI, config.HAAuthToken) if err != nil { + ctxCancel() return nil, err } diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 8d7985b..001ce1c 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -11,7 +11,6 @@ import ( "fmt" "log/slog" "sync" - "time" "github.com/gorilla/websocket" i "saml.dev/gome-assistant/internal" @@ -49,53 +48,47 @@ func ReadMessage(conn *websocket.Conn) ([]byte, error) { return msg, nil } -func SetupConnection(ip, port, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) { +func SetupConnection(ctx context.Context, ip, port, authToken string) (*websocket.Conn, error) { uri := fmt.Sprintf("ws://%s:%s/api/websocket", ip, port) - return ConnectionFromUri(uri, authToken) + return ConnectionFromUri(ctx, uri, authToken) } -func SetupSecureConnection(ip, port, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) { +func SetupSecureConnection(ctx context.Context, ip, port, authToken string) (*websocket.Conn, error) { uri := fmt.Sprintf("wss://%s:%s/api/websocket", ip, port) - return ConnectionFromUri(uri, authToken) + return ConnectionFromUri(ctx, uri, authToken) } -func ConnectionFromUri(uri, authToken string) (*websocket.Conn, context.Context, context.CancelFunc, error) { - ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*3) - +func ConnectionFromUri(ctx context.Context, uri, authToken string) (*websocket.Conn, error) { // Init websocket connection dialer := websocket.DefaultDialer conn, _, err := dialer.DialContext(ctx, uri, nil) if err != nil { - ctxCancel() slog.Error("Failed to connect to websocket. Check URI\n", "uri", uri) - return nil, nil, nil, err + return nil, err } // Read auth_required message _, err = ReadMessage(conn) if err != nil { - ctxCancel() slog.Error("Unknown error creating websocket client\n") - return nil, nil, nil, err + return nil, err } // Send auth message err = SendAuthMessage(conn, authToken) if err != nil { - ctxCancel() slog.Error("Unknown error creating websocket client\n") - return nil, nil, nil, err + return nil, err } // Verify auth message was successful err = VerifyAuthResponse(conn) if err != nil { - ctxCancel() slog.Error("Auth token is invalid. Please double check it or create a new token in your Home Assistant profile\n") - return nil, nil, nil, err + return nil, err } - return conn, ctx, ctxCancel, nil + return conn, nil } func SendAuthMessage(conn *websocket.Conn, token string) error { From a11cb4fa9cd59c2a5a63be591d6f5324415ffe06 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 15:38:00 +0100 Subject: [PATCH 007/103] App: remove the `ctx` member Instead of creating a context in `NewAppFromConfig()` and `NewApp()`, accept a context as argument and use it only for connecting. (It was never used for anything else anyway.) --- app.go | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/app.go b/app.go index 87478ef..81fa2d2 100644 --- a/app.go +++ b/app.go @@ -22,9 +22,7 @@ var ErrInvalidToken = ws.ErrInvalidToken var ErrInvalidArgs = errors.New("invalid arguments provided") type App struct { - ctx context.Context - ctxCancel context.CancelFunc - conn *websocket.Conn + conn *websocket.Conn // Wraps the ws connection with added mutex locking wsWriter *ws.WebsocketWriter @@ -88,22 +86,20 @@ type NewAppConfig struct { HomeZoneEntityId string } -/* -NewAppFromConfig establishes the websocket connection and returns an -object you can use to register schedules and listeners, based on the -URIs that it should connect to. -*/ -func NewAppFromConfig(config NewAppConfig) (*App, error) { +// NewAppFromConfig establishes the websocket connection and returns +// an object you can use to register schedules and listeners, based on +// the URIs that it should connect to. `ctx` is used only to limit the +// time spent connecting; it cannot be used after that to cancel the +// app. +func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { if config.RESTBaseURI == "" || config.WebsocketURI == "" || config.HAAuthToken == "" || config.HomeZoneEntityId == "" { slog.Error("RESTBaseURI, WebsocketURI, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") return nil, ErrInvalidArgs } - ctx, ctxCancel := context.WithTimeout(context.Background(), time.Second*3) conn, err := ws.ConnectionFromUri(ctx, config.WebsocketURI, config.HAAuthToken) if err != nil { - ctxCancel() return nil, err } @@ -119,8 +115,6 @@ func NewAppFromConfig(config NewAppConfig) (*App, error) { return &App{ conn: conn, wsWriter: wsWriter, - ctx: ctx, - ctxCancel: ctxCancel, httpClient: httpClient, service: service, state: state, @@ -159,11 +153,11 @@ type NewAppRequest struct { Secure bool } -/* -NewApp establishes the websocket connection and returns an object -you can use to register schedules and listeners. -*/ -func NewApp(request NewAppRequest) (*App, error) { +// NewApp establishes the websocket connection and returns an object +// you can use to register schedules and listeners. `ctx` is used only +// to limit the time spent connecting; it cannot be used after that to +// cancel the app. +func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { if request.IpAddress == "" || request.HAAuthToken == "" || request.HomeZoneEntityId == "" { slog.Error("IpAddress, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") return nil, ErrInvalidArgs @@ -186,13 +180,10 @@ func NewApp(request NewAppRequest) (*App, error) { config.RESTBaseURI = fmt.Sprintf("http://%s:%s/api", request.IpAddress, port) } - return NewAppFromConfig(config) + return NewAppFromConfig(ctx, config) } func (a *App) Cleanup() { - if a.ctxCancel != nil { - a.ctxCancel() - } } func (a *App) RegisterSchedules(schedules ...DailySchedule) { From 7071b5518452276ab2ef67e96523f88bae5b305b Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 17:03:40 +0100 Subject: [PATCH 008/103] runSchedules(): simplify loop code --- schedule.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/schedule.go b/schedule.go index 77a26d5..0e90a0b 100644 --- a/schedule.go +++ b/schedule.go @@ -159,17 +159,10 @@ func runSchedules(a *App) { for { sched := popSchedule(a) - - // run callback for all schedules before now in case they overlap - for sched.nextRunTime.Before(time.Now()) { - sched.maybeRunCallback(a) - requeueSchedule(a, sched) - - sched = popSchedule(a) + if sched.nextRunTime.After(time.Now()) { + slog.Info("Next schedule", "start_time", sched.nextRunTime) + time.Sleep(time.Until(sched.nextRunTime)) } - - slog.Info("Next schedule", "start_time", sched.nextRunTime) - time.Sleep(time.Until(sched.nextRunTime)) sched.maybeRunCallback(a) requeueSchedule(a, sched) } From 680cf67bac6229fc7a49e0b4a8faf47645e9162d Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 17:23:46 +0100 Subject: [PATCH 009/103] runIntervals(): simplify loop code --- interval.go | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/interval.go b/interval.go index 3d525d9..084400c 100644 --- a/interval.go +++ b/interval.go @@ -152,16 +152,9 @@ func runIntervals(a *App) { for { i := popInterval(a) - - // run callback for all intervals before now in case they overlap - for i.nextRunTime.Before(time.Now()) { - i.maybeRunCallback(a) - requeueInterval(a, i) - - i = popInterval(a) + if i.nextRunTime.After(time.Now()) { + time.Sleep(time.Until(i.nextRunTime)) } - - time.Sleep(time.Until(i.nextRunTime)) i.maybeRunCallback(a) requeueInterval(a, i) } From d035b67f45de188b2a56c5ae89659ea20c39d33c Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 17:25:54 +0100 Subject: [PATCH 010/103] runIntervals(), runSchedules(): convert functions in to methods Make these two functions methods of `App`. --- app.go | 4 ++-- interval.go | 3 +-- schedule.go | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 81fa2d2..4648ffc 100644 --- a/app.go +++ b/app.go @@ -304,8 +304,8 @@ func (a *App) Start() { slog.Info("Starting", "entity listeners", len(a.entityListeners)) slog.Info("Starting", "event listeners", len(a.eventListeners)) - go runSchedules(a) - go runIntervals(a) + go a.runSchedules() + go a.runIntervals() // subscribe to state_changed events id := internal.GetId() diff --git a/interval.go b/interval.go index 084400c..574cbb3 100644 --- a/interval.go +++ b/interval.go @@ -144,8 +144,7 @@ func (sb intervalBuilderEnd) Build() Interval { return sb.interval } -// app.Start() functions -func runIntervals(a *App) { +func (a *App) runIntervals() { if a.intervals.Len() == 0 { return } diff --git a/schedule.go b/schedule.go index 0e90a0b..96cee06 100644 --- a/schedule.go +++ b/schedule.go @@ -152,7 +152,7 @@ func (sb scheduleBuilderEnd) Build() DailySchedule { } // app.Start() functions -func runSchedules(a *App) { +func (a *App) runSchedules() { if a.schedules.Len() == 0 { return } From 9190da49ecb73ae9ad076cbe84f0af1e726303b8 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 17:52:40 +0100 Subject: [PATCH 011/103] Interval, DailySchedule: extract `shouldRun()` methods --- interval.go | 19 +++++++++++++------ schedule.go | 15 +++++++++++---- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/interval.go b/interval.go index 574cbb3..21ce1bc 100644 --- a/interval.go +++ b/interval.go @@ -159,23 +159,30 @@ func (a *App) runIntervals() { } } -func (i Interval) maybeRunCallback(a *App) { +func (i Interval) shouldRun(a *App) bool { if c := checkStartEndTime(i.startTime /* isStart = */, true); c.fail { - return + return false } if c := checkStartEndTime(i.endTime /* isStart = */, false); c.fail { - return + return false } if c := checkExceptionDates(i.exceptionDates); c.fail { - return + return false } if c := checkExceptionRanges(i.exceptionRanges); c.fail { - return + return false } if c := checkEnabledEntity(a.state, i.enabledEntities); c.fail { - return + return false } if c := checkDisabledEntity(a.state, i.disabledEntities); c.fail { + return false + } + return true +} + +func (i Interval) maybeRunCallback(a *App) { + if !i.shouldRun(a) { return } go i.callback(a.service, a.state) diff --git a/schedule.go b/schedule.go index 96cee06..9e8f72d 100644 --- a/schedule.go +++ b/schedule.go @@ -168,17 +168,24 @@ func (a *App) runSchedules() { } } -func (s DailySchedule) maybeRunCallback(a *App) { +func (s DailySchedule) shouldRun(a *App) bool { if c := checkExceptionDates(s.exceptionDates); c.fail { - return + return false } if c := checkAllowlistDates(s.allowlistDates); c.fail { - return + return false } if c := checkEnabledEntity(a.state, s.enabledEntities); c.fail { - return + return false } if c := checkDisabledEntity(a.state, s.disabledEntities); c.fail { + return false + } + return true +} + +func (s DailySchedule) maybeRunCallback(a *App) { + if !s.shouldRun(a) { return } go s.callback(a.service, a.state) From 9261f6196cad2e1281930336eb175a37a72fb293 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 18:58:19 +0100 Subject: [PATCH 012/103] Handle schedules and intervals using shared code Introduce a new interface, `scheduledAction`, and make `*DailySchedule` and `*Interval` both implement it. Change `App` to have a single queue of `scheduledAction`s. --- app.go | 117 ++++++++++++++++++++++++++++++++-------------------- interval.go | 53 +++++++++++------------- schedule.go | 60 ++++++++++++--------------- 3 files changed, 123 insertions(+), 107 deletions(-) diff --git a/app.go b/app.go index 4648ffc..24d29e1 100644 --- a/app.go +++ b/app.go @@ -32,8 +32,7 @@ type App struct { service *Service state *StateImpl - schedules pq.PriorityQueue - intervals pq.PriorityQueue + scheduledActions pq.PriorityQueue entityListeners map[string][]*EntityListener entityListenersId int64 eventListeners map[string][]*EventListener @@ -113,15 +112,14 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { } return &App{ - conn: conn, - wsWriter: wsWriter, - httpClient: httpClient, - service: service, - state: state, - schedules: pq.New(), - intervals: pq.New(), - entityListeners: map[string][]*EntityListener{}, - eventListeners: map[string][]*EventListener{}, + conn: conn, + wsWriter: wsWriter, + httpClient: httpClient, + service: service, + state: state, + scheduledActions: pq.New(), + entityListeners: map[string][]*EntityListener{}, + eventListeners: map[string][]*EventListener{}, }, nil } @@ -186,41 +184,30 @@ func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { func (a *App) Cleanup() { } -func (a *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.schedules.Insert(s, float64(s.nextRunTime.Unix())) - continue - } - - now := carbon.Now() - startTime := carbon.Now().SetTimeMilli(s.hour, s.minute, 0, 0) +type scheduledAction interface { + String() string + Hash() string + initializeNextRunTime(a *App) + shouldRun(a *App) bool + run(a *App) + updateNextRunTime(a *App) + getNextRunTime() time.Time +} - // advance first scheduled time by frequency until it is in the future - if startTime.Lt(now) { - startTime = startTime.AddDay() - } +func (a *App) RegisterScheduledAction(action scheduledAction) { + action.initializeNextRunTime(a) + a.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) +} - s.nextRunTime = startTime.Carbon2Time() - a.schedules.Insert(s, float64(startTime.Carbon2Time().Unix())) +func (a *App) RegisterSchedules(schedules ...*DailySchedule) { + for _, s := range schedules { + a.RegisterScheduledAction(s) } } -func (a *App) RegisterIntervals(intervals ...Interval) { +func (a *App) RegisterIntervals(intervals ...*Interval) { for _, i := range intervals { - if i.frequency == 0 { - slog.Error("A schedule must use either set frequency via Every()") - panic(ErrInvalidArgs) - } - - i.nextRunTime = internal.ParseTime(string(i.startTime)).Carbon2Time() - now := time.Now() - for i.nextRunTime.Before(now) { - i.nextRunTime = i.nextRunTime.Add(i.frequency) - } - a.intervals.Insert(i, float64(i.nextRunTime.Unix())) + a.RegisterScheduledAction(i) } } @@ -299,13 +286,14 @@ func getNextSunRiseOrSet(a *App, sunrise bool, offset ...DurationString) carbon. return sunriseOrSunset } -func (a *App) Start() { - slog.Info("Starting", "schedules", a.schedules.Len()) +// Start the app. When `ctx` expires, the app closes the connection +// and returns. +func (a *App) Start(ctx context.Context) { + slog.Info("Starting", "scheduled actions", a.scheduledActions.Len()) slog.Info("Starting", "entity listeners", len(a.entityListeners)) slog.Info("Starting", "event listeners", len(a.eventListeners)) - go a.runSchedules() - go a.runIntervals() + go a.runScheduledActions(ctx) // subscribe to state_changed events id := internal.GetId() @@ -360,3 +348,44 @@ func (a *App) GetService() *Service { func (a *App) GetState() State { return a.state } + +func (a *App) runScheduledActions(ctx context.Context) { + if a.scheduledActions.Len() == 0 { + return + } + + // Create a new, but stopped, timer: + timer := time.NewTimer(1 * time.Hour) + if !timer.Stop() { + <-timer.C + } + + for { + action := a.popScheduledAction() + if action.getNextRunTime().After(time.Now()) { + timer.Reset(time.Until(action.getNextRunTime())) + + select { + case <-timer.C: + case <-ctx.Done(): + return + } + } + + if action.shouldRun(a) { + go action.run(a) + } + + a.requeueScheduledAction(action) + } +} + +func (a *App) popScheduledAction() scheduledAction { + action, _ := a.scheduledActions.Pop() + return action.(scheduledAction) +} + +func (a *App) requeueScheduledAction(action scheduledAction) { + action.updateNextRunTime(a) + a.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) +} diff --git a/interval.go b/interval.go index 21ce1bc..4635ac9 100644 --- a/interval.go +++ b/interval.go @@ -2,6 +2,7 @@ package gomeassistant import ( "fmt" + "log/slog" "time" "saml.dev/gome-assistant/internal" @@ -23,28 +24,28 @@ type Interval struct { disabledEntities []internal.EnabledDisabledInfo } -func (i Interval) Hash() string { +func (i *Interval) Hash() string { return fmt.Sprint(i.startTime, i.endTime, i.frequency, i.callback, i.exceptionDates, i.exceptionRanges) } // Call type intervalBuilder struct { - interval Interval + interval *Interval } // Every type intervalBuilderCall struct { - interval Interval + interval *Interval } // Offset, ExceptionDates, ExceptionRange type intervalBuilderEnd struct { - interval Interval + interval *Interval } func NewInterval() intervalBuilder { return intervalBuilder{ - Interval{ + &Interval{ frequency: 0, startTime: "00:00", endTime: "00:00", @@ -52,7 +53,7 @@ func NewInterval() intervalBuilder { } } -func (i Interval) String() string { +func (i *Interval) String() string { return fmt.Sprintf("Interval{ call %q every %s%s%s }", internal.GetFunctionName(i.callback), i.frequency, @@ -140,25 +141,27 @@ func (ib intervalBuilderEnd) DisabledWhen(entityId, state string, runOnNetworkEr return ib } -func (sb intervalBuilderEnd) Build() Interval { +func (sb intervalBuilderEnd) Build() *Interval { return sb.interval } -func (a *App) runIntervals() { - if a.intervals.Len() == 0 { - return +func (i *Interval) initializeNextRunTime(a *App) { + if i.frequency == 0 { + slog.Error("A schedule must use either set frequency via Every()") + panic(ErrInvalidArgs) } - for { - i := popInterval(a) - if i.nextRunTime.After(time.Now()) { - time.Sleep(time.Until(i.nextRunTime)) - } - i.maybeRunCallback(a) - requeueInterval(a, i) + i.nextRunTime = internal.ParseTime(string(i.startTime)).Carbon2Time() + now := time.Now() + for i.nextRunTime.Before(now) { + i.nextRunTime = i.nextRunTime.Add(i.frequency) } } +func (i *Interval) getNextRunTime() time.Time { + return i.nextRunTime +} + func (i Interval) shouldRun(a *App) bool { if c := checkStartEndTime(i.startTime /* isStart = */, true); c.fail { return false @@ -181,20 +184,10 @@ func (i Interval) shouldRun(a *App) bool { return true } -func (i Interval) maybeRunCallback(a *App) { - if !i.shouldRun(a) { - return - } - go i.callback(a.service, a.state) -} - -func popInterval(a *App) Interval { - i, _ := a.intervals.Pop() - return i.(Interval) +func (i *Interval) run(a *App) { + i.callback(a.service, a.state) } -func requeueInterval(a *App, i Interval) { +func (i *Interval) updateNextRunTime(a *App) { i.nextRunTime = i.nextRunTime.Add(i.frequency) - - a.intervals.Insert(i, float64(i.nextRunTime.Unix())) } diff --git a/schedule.go b/schedule.go index 9e8f72d..7b2aeeb 100644 --- a/schedule.go +++ b/schedule.go @@ -2,7 +2,6 @@ package gomeassistant import ( "fmt" - "log/slog" "time" "github.com/golang-module/carbon" @@ -31,25 +30,25 @@ type DailySchedule struct { disabledEntities []internal.EnabledDisabledInfo } -func (s DailySchedule) Hash() string { +func (s *DailySchedule) Hash() string { return fmt.Sprint(s.hour, s.minute, s.callback) } type scheduleBuilder struct { - schedule DailySchedule + schedule *DailySchedule } type scheduleBuilderCall struct { - schedule DailySchedule + schedule *DailySchedule } type scheduleBuilderEnd struct { - schedule DailySchedule + schedule *DailySchedule } func NewDailySchedule() scheduleBuilder { return scheduleBuilder{ - DailySchedule{ + &DailySchedule{ hour: 0, minute: 0, sunOffset: "0s", @@ -57,7 +56,7 @@ func NewDailySchedule() scheduleBuilder { } } -func (s DailySchedule) String() string { +func (s *DailySchedule) String() string { return fmt.Sprintf("Schedule{ call %q daily at %s }", internal.GetFunctionName(s.callback), stringHourMinute(s.hour, s.minute), @@ -147,28 +146,33 @@ func (sb scheduleBuilderEnd) DisabledWhen(entityId, state string, runOnNetworkEr return sb } -func (sb scheduleBuilderEnd) Build() DailySchedule { +func (sb scheduleBuilderEnd) Build() *DailySchedule { return sb.schedule } -// app.Start() functions -func (a *App) runSchedules() { - if a.schedules.Len() == 0 { +func (s *DailySchedule) initializeNextRunTime(a *App) { + // realStartTime already set for sunset/sunrise + if s.isSunrise || s.isSunset { + s.nextRunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset).Carbon2Time() return } - for { - sched := popSchedule(a) - if sched.nextRunTime.After(time.Now()) { - slog.Info("Next schedule", "start_time", sched.nextRunTime) - time.Sleep(time.Until(sched.nextRunTime)) - } - sched.maybeRunCallback(a) - requeueSchedule(a, sched) + now := carbon.Now() + startTime := carbon.Now().SetTimeMilli(s.hour, s.minute, 0, 0) + + // advance first scheduled time by frequency until it is in the future + if startTime.Lt(now) { + startTime = startTime.AddDay() } + + s.nextRunTime = startTime.Carbon2Time() +} + +func (s *DailySchedule) getNextRunTime() time.Time { + return s.nextRunTime } -func (s DailySchedule) shouldRun(a *App) bool { +func (s *DailySchedule) shouldRun(a *App) bool { if c := checkExceptionDates(s.exceptionDates); c.fail { return false } @@ -184,19 +188,11 @@ func (s DailySchedule) shouldRun(a *App) bool { return true } -func (s DailySchedule) maybeRunCallback(a *App) { - if !s.shouldRun(a) { - return - } - go s.callback(a.service, a.state) +func (s *DailySchedule) run(a *App) { + s.callback(a.service, a.state) } -func popSchedule(a *App) DailySchedule { - _sched, _ := a.schedules.Pop() - return _sched.(DailySchedule) -} - -func requeueSchedule(a *App, s DailySchedule) { +func (s *DailySchedule) updateNextRunTime(a *App) { if s.isSunrise || s.isSunset { var nextSunTime carbon.Carbon // "0s" is default value @@ -210,6 +206,4 @@ func requeueSchedule(a *App, s DailySchedule) { } else { s.nextRunTime = carbon.Time2Carbon(s.nextRunTime).AddDay().Carbon2Time() } - - a.schedules.Insert(s, float64(s.nextRunTime.Unix())) } From 803e4ea78d0a56922c9fab7cbe6f365494105532 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Fri, 29 Mar 2024 20:50:02 +0100 Subject: [PATCH 013/103] Work on cleanup of goroutines --- app.go | 108 ++++++++++++++++++++++++++++++++++++++++----------------- go.mod | 1 + go.sum | 2 ++ 3 files changed, 80 insertions(+), 31 deletions(-) diff --git a/app.go b/app.go index 24d29e1..959c09c 100644 --- a/app.go +++ b/app.go @@ -5,11 +5,13 @@ import ( "errors" "fmt" "log/slog" + "sync" "time" "github.com/golang-module/carbon" "github.com/gorilla/websocket" sunriseLib "github.com/nathan-osman/go-sunrise" + "golang.org/x/sync/errgroup" "saml.dev/gome-assistant/internal" "saml.dev/gome-assistant/internal/http" pq "saml.dev/gome-assistant/internal/priorityqueue" @@ -36,17 +38,19 @@ type App struct { entityListeners map[string][]*EntityListener entityListenersId int64 eventListeners map[string][]*EventListener + + // If `App.Start()` has been called, `cancel()` cancels the + // context being used, which causes the app to shut down cleanly. + cancel context.CancelFunc + + closeOnce sync.Once } -/* -DurationString represents a duration, such as "2s" or "24h". -See https://pkg.go.dev/time#ParseDuration for all valid time units. -*/ +// DurationString represents a duration, such as "2s" or "24h". See +// https://pkg.go.dev/time#ParseDuration for all valid time units. type DurationString string -/* -TimeString is a 24-hr format time "HH:MM" such as "07:30". -*/ +// TimeString is a 24-hr format time "HH:MM" such as "07:30". type TimeString string type timeRange struct { @@ -120,6 +124,7 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { scheduledActions: pq.New(), entityListeners: map[string][]*EntityListener{}, eventListeners: map[string][]*EventListener{}, + cancel: func() {}, }, nil } @@ -154,7 +159,8 @@ type NewAppRequest struct { // NewApp establishes the websocket connection and returns an object // you can use to register schedules and listeners. `ctx` is used only // to limit the time spent connecting; it cannot be used after that to -// cancel the app. +// cancel the app. If this function returns successfully, then +// `App.Close()` must eventually be called to release resources. func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { if request.IpAddress == "" || request.HAAuthToken == "" || request.HomeZoneEntityId == "" { slog.Error("IpAddress, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") @@ -181,9 +187,6 @@ func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { return NewAppFromConfig(ctx, config) } -func (a *App) Cleanup() { -} - type scheduledAction interface { String() string Hash() string @@ -289,11 +292,20 @@ func getNextSunRiseOrSet(a *App, sunrise bool, offset ...DurationString) carbon. // Start the app. When `ctx` expires, the app closes the connection // and returns. func (a *App) Start(ctx context.Context) { + ctx, cancel := context.WithCancel(ctx) + a.cancel = cancel + defer cancel() + + eg, ctx := errgroup.WithContext(ctx) + slog.Info("Starting", "scheduled actions", a.scheduledActions.Len()) slog.Info("Starting", "entity listeners", len(a.entityListeners)) slog.Info("Starting", "event listeners", len(a.eventListeners)) - go a.runScheduledActions(ctx) + eg.Go(func() error { + a.runScheduledActions(ctx) + return nil + }) // subscribe to state_changed events id := internal.GetId() @@ -312,13 +324,17 @@ func (a *App) Start(ctx context.Context) { } etl.runOnStartupCompleted = true - go etl.callback(a.service, a.state, EntityData{ - TriggerEntityId: eid, - FromState: entityState.State, - FromAttributes: entityState.Attributes, - ToState: entityState.State, - ToAttributes: entityState.Attributes, - LastChanged: entityState.LastChanged, + etl := etl + eg.Go(func() error { + etl.callback(a.service, a.state, EntityData{ + TriggerEntityId: eid, + FromState: entityState.State, + FromAttributes: entityState.Attributes, + ToState: entityState.State, + ToAttributes: entityState.Attributes, + LastChanged: entityState.LastChanged, + }) + return nil }) } } @@ -326,19 +342,49 @@ func (a *App) Start(ctx context.Context) { // entity listeners and event listeners elChan := make(chan ws.ChanMsg) - go ws.ListenWebsocket(a.conn, elChan) - - for { - msg, ok := <-elChan - if !ok { - break - } - if a.entityListenersId == msg.Id { - go callEntityListeners(a, msg.Raw) - } else { - go callEventListeners(a, msg) + eg.Go(func() error { + ws.ListenWebsocket(a.conn, elChan) + cancel() + return nil + }) + + eg.Go(func() error { + for { + msg, ok := <-elChan + if !ok { + break + } + if a.entityListenersId == msg.Id { + go callEntityListeners(a, msg.Raw) + } else { + go callEventListeners(a, msg) + } } - } + return nil + }) + + eg.Go(func() error { + <-ctx.Done() + a.Close() + return nil + }) + + eg.Wait() +} + +// Close closes the connection and releases any resources. It may be +// called more than once; only the first call does anything. +func (a *App) Close() { + a.closeOnce.Do(func() { + a.close() + }) +} + +// close closes the connection and releases resources. It must be +// called exactly once. +func (a *App) close() { + a.cancel() + a.conn.Close() } func (a *App) GetService() *Service { diff --git a/go.mod b/go.mod index 4c9b4d1..8d72e98 100644 --- a/go.mod +++ b/go.mod @@ -18,5 +18,6 @@ require ( github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/testify v1.8.4 // indirect + golang.org/x/sync v0.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index e5e5f2f..54329c7 100644 --- a/go.sum +++ b/go.sum @@ -75,6 +75,8 @@ golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8U golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= From 2459f0e466f5a6128dfde2f4af5a69a62ad252de Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 20:52:31 +0200 Subject: [PATCH 014/103] Make example compile --- example/.gitignore | 1 + example/example.go | 21 +++++--- example/example_live_test.go | 22 ++++---- example/go.mod | 24 --------- example/go.sum | 100 ----------------------------------- go.mod | 5 +- go.sum | 2 + 7 files changed, 33 insertions(+), 142 deletions(-) create mode 100644 example/.gitignore delete mode 100644 example/go.mod delete mode 100644 example/go.sum diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..6f30a3a --- /dev/null +++ b/example/.gitignore @@ -0,0 +1 @@ +/example diff --git a/example/example.go b/example/example.go index 6d77d46..60fc713 100644 --- a/example/example.go +++ b/example/example.go @@ -1,6 +1,7 @@ -package example +package main import ( + "context" "encoding/json" "log/slog" "os" @@ -10,17 +11,21 @@ import ( ) func main() { - app, err := ga.NewApp(ga.NewAppRequest{ - IpAddress: "192.168.86.67", // Replace with your Home Assistant IP Address - HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), - HomeZoneEntityId: "zone.home", - }) + ctx := context.Background() + app, err := ga.NewApp( + ctx, + ga.NewAppRequest{ + IpAddress: "192.168.86.67", // Replace with your Home Assistant IP Address + HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), + HomeZoneEntityId: "zone.home", + }, + ) if err != nil { slog.Error("Error connecting to HASS:", err) os.Exit(1) } - defer app.Cleanup() + defer app.Close() pantryDoor := ga. NewEntityListener(). @@ -50,7 +55,7 @@ func main() { app.RegisterSchedules(_11pmSched, _30minsBeforeSunrise) app.RegisterEventListeners(zwaveEventListener) - app.Start() + app.Start(ctx) } func pantryLights(service *ga.Service, state ga.State, sensor ga.EntityData) { diff --git a/example/example_live_test.go b/example/example_live_test.go index e636a22..37bc07c 100644 --- a/example/example_live_test.go +++ b/example/example_live_test.go @@ -1,6 +1,7 @@ -package example +package main import ( + "context" "log/slog" "os" "testing" @@ -45,7 +46,7 @@ func setupLogging() { slog.SetDefault(slog.New(devslog.NewHandler(os.Stdout, opts))) } -func (s *MySuite) SetupSuite() { +func (s *MySuite) SetupSuite(ctx context.Context) { setupLogging() slog.Debug("Setting up test suite...") s.suiteCtx = make(map[string]any) @@ -61,11 +62,14 @@ func (s *MySuite) SetupSuite() { slog.Error("Error unmarshalling config file", err) } - s.app, err = ga.NewApp(ga.NewAppRequest{ - HAAuthToken: s.config.Hass.HAAuthToken, - IpAddress: s.config.Hass.IpAddress, - HomeZoneEntityId: s.config.Hass.HomeZoneEntityId, - }) + s.app, err = ga.NewApp( + ctx, + ga.NewAppRequest{ + HAAuthToken: s.config.Hass.HAAuthToken, + IpAddress: s.config.Hass.IpAddress, + HomeZoneEntityId: s.config.Hass.HomeZoneEntityId, + }, + ) if err != nil { slog.Error("Failed to createw new app", err) s.T().FailNow() @@ -85,12 +89,12 @@ func (s *MySuite) SetupSuite() { s.app.RegisterSchedules(dailySchedule) // start GA app - go s.app.Start() + go s.app.Start(ctx) } func (s *MySuite) TearDownSuite() { if s.app != nil { - s.app.Cleanup() + s.app.Close() s.app = nil } } diff --git a/example/go.mod b/example/go.mod deleted file mode 100644 index 49184a0..0000000 --- a/example/go.mod +++ /dev/null @@ -1,24 +0,0 @@ -module example - -go 1.21 - -require ( - github.com/golang-cz/devslog v0.0.8 - github.com/stretchr/testify v1.8.4 - gopkg.in/yaml.v3 v3.0.1 - saml.dev/gome-assistant v0.2.0 -) - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gobuffalo/envy v1.10.2 // indirect - github.com/gobuffalo/packd v1.0.2 // indirect - github.com/gobuffalo/packr v1.30.1 // indirect - github.com/golang-module/carbon v1.7.3 // indirect - github.com/gorilla/websocket v1.5.0 // indirect - github.com/joho/godotenv v1.5.1 // indirect - github.com/nathan-osman/go-sunrise v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.11.0 // indirect - golang.org/x/mod v0.9.0 // indirect -) diff --git a/example/go.sum b/example/go.sum deleted file mode 100644 index 232d6d1..0000000 --- a/example/go.sum +++ /dev/null @@ -1,100 +0,0 @@ -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI= -github.com/gobuffalo/envy v1.10.2 h1:EIi03p9c3yeuRCFPOKcSfajzkLb3hrRjEpHGI8I2Wo4= -github.com/gobuffalo/envy v1.10.2/go.mod h1:qGAGwdvDsaEtPhfBzb3o0SfDea8ByGn9j8bKmVft9z8= -github.com/gobuffalo/logger v1.0.0/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs= -github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q= -github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw= -github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= -github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= -github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= -github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= -github.com/golang-cz/devslog v0.0.8 h1:53ipA2rC5JzWBWr9qB8EfenvXppenNiF/8DwgtNT5Q4= -github.com/golang-cz/devslog v0.0.8/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= -github.com/golang-module/carbon v1.7.3 h1:p5mUZj7Tg62MblrkF7XEoxVPvhVs20N/kimqsZOQ+/U= -github.com/golang-module/carbon v1.7.3/go.mod h1:nUMnXq90Rv8a7h2+YOo2BGKS77Y0w/hMPm4/a8h19N8= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= -github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/karrick/godirwalk v1.10.12/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/nathan-osman/go-sunrise v1.1.0 h1:ZqZmtmtzs8Os/DGQYi0YMHpuUqR/iRoJK+wDO0wTCw8= -github.com/nathan-osman/go-sunrise v1.1.0/go.mod h1:RcWqhT+5ShCZDev79GuWLayetpJp78RSjSWxiDowmlM= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -saml.dev/gome-assistant v0.2.0 h1:Clo5DrziTdsYydVUTQfroeBVmToMnNHoObr+k6HhbMY= -saml.dev/gome-assistant v0.2.0/go.mod h1:jsZUtnxANCP0zB2B7iyy4j7sZohMGop8g+5EB2MER3o= diff --git a/go.mod b/go.mod index 8d72e98..92e2948 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module saml.dev/gome-assistant -go 1.21 +go 1.21.0 + +toolchain go1.21.6 require ( github.com/golang-module/carbon v1.7.1 @@ -13,6 +15,7 @@ require ( github.com/gobuffalo/envy v1.10.2 // indirect github.com/gobuffalo/packd v1.0.2 // indirect github.com/gobuffalo/packr v1.30.1 // indirect + github.com/golang-cz/devslog v0.0.8 // indirect github.com/joho/godotenv v1.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect diff --git a/go.sum b/go.sum index 54329c7..f34677d 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7 github.com/gobuffalo/packr v1.30.1 h1:hu1fuVR3fXEZR7rXNW3h8rqSML8EVAf6KNm0NKO/wKg= github.com/gobuffalo/packr v1.30.1/go.mod h1:ljMyFO2EcrnzsHsN99cvbq055Y9OhRrIaviy289eRuk= github.com/gobuffalo/packr/v2 v2.5.1/go.mod h1:8f9c96ITobJlPzI44jj+4tHnEKNt0xXWSVlXRN9X1Iw= +github.com/golang-cz/devslog v0.0.8 h1:53ipA2rC5JzWBWr9qB8EfenvXppenNiF/8DwgtNT5Q4= +github.com/golang-cz/devslog v0.0.8/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8= github.com/golang-module/carbon v1.7.1 h1:EDPV0YjxeS2kE2cRedfGgDikU6l5D79HB/teHuZDLu8= github.com/golang-module/carbon v1.7.1/go.mod h1:M/TDTYPp3qWtW68u49dLDJOyGmls6L6BXdo/pyvkMaU= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= From 516b5af5994c9c7d0acff2eb949b98f62763786f Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 20:52:59 +0200 Subject: [PATCH 015/103] ListenWebsocket(): add comment, tighten signature --- internal/websocket/reader.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index 06704a0..4d280a7 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -14,19 +14,22 @@ type BaseMessage struct { } type ChanMsg struct { - Id int64 Type string + Id int64 Success bool Raw []byte } -func ListenWebsocket(conn *websocket.Conn, c chan ChanMsg) { +// ListenWebsocket reads JSON-formatted messages from `conn`, partly +// deserializes them, and sends them to `c`. If there is an error, it +// closes `c` and returns. +func ListenWebsocket(conn *websocket.Conn, c chan<- ChanMsg) { for { bytes, err := ReadMessage(conn) if err != nil { slog.Error("Error reading from websocket:", err) close(c) - break + return } base := BaseMessage{ From 311327fa778419cc4ee802cf81f249825743ab2b Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:06:51 +0200 Subject: [PATCH 016/103] Remove unnecessary abbreviation --- internal/websocket/websocket.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 001ce1c..b559ae1 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -13,7 +13,8 @@ import ( "sync" "github.com/gorilla/websocket" - i "saml.dev/gome-assistant/internal" + + "saml.dev/gome-assistant/internal" ) var ErrInvalidToken = errors.New("invalid authentication token") @@ -133,7 +134,7 @@ func SubscribeToStateChangedEvents(id int64, conn *WebsocketWriter) { func SubscribeToEventType(eventType string, conn *WebsocketWriter, id ...int64) { var finalId int64 if len(id) == 0 { - finalId = i.GetId() + finalId = internal.GetId() } else { finalId = id[0] } From feabb049c0f1a773a57d76187c9d99a1478afabb Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:12:27 +0200 Subject: [PATCH 017/103] WebsocketConn: type renamed from `WebsocketWriter` Let's make it the only interface to the `websocket.Conn`. --- app.go | 10 +++++----- internal/services/alarm_control_panel.go | 2 +- internal/services/climate.go | 2 +- internal/services/cover.go | 2 +- internal/services/event.go | 2 +- internal/services/homeassistant.go | 2 +- internal/services/input_boolean.go | 2 +- internal/services/input_button.go | 2 +- internal/services/input_datetime.go | 2 +- internal/services/input_number.go | 2 +- internal/services/input_text.go | 2 +- internal/services/light.go | 2 +- internal/services/lock.go | 2 +- internal/services/media_player.go | 2 +- internal/services/notify.go | 2 +- internal/services/number.go | 2 +- internal/services/scene.go | 2 +- internal/services/script.go | 2 +- internal/services/services.go | 2 +- internal/services/switch.go | 2 +- internal/services/tts.go | 2 +- internal/services/vacuum.go | 2 +- internal/services/zwavejs.go | 2 +- internal/websocket/websocket.go | 8 ++++---- service.go | 2 +- 25 files changed, 32 insertions(+), 32 deletions(-) diff --git a/app.go b/app.go index 959c09c..5676ec1 100644 --- a/app.go +++ b/app.go @@ -27,7 +27,7 @@ type App struct { conn *websocket.Conn // Wraps the ws connection with added mutex locking - wsWriter *ws.WebsocketWriter + wsConn *ws.WebsocketConn httpClient *http.HttpClient @@ -108,7 +108,7 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { httpClient := http.ClientFromUri(config.RESTBaseURI, config.HAAuthToken) - wsWriter := &ws.WebsocketWriter{Conn: conn} + wsWriter := &ws.WebsocketConn{Conn: conn} service := newService(wsWriter, httpClient) state, err := newState(httpClient, config.HomeZoneEntityId) if err != nil { @@ -117,7 +117,7 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { return &App{ conn: conn, - wsWriter: wsWriter, + wsConn: wsWriter, httpClient: httpClient, service: service, state: state, @@ -239,7 +239,7 @@ func (a *App) RegisterEventListeners(evls ...EventListener) { if elList, ok := a.eventListeners[eventType]; ok { a.eventListeners[eventType] = append(elList, &evl) } else { - ws.SubscribeToEventType(eventType, a.wsWriter) + ws.SubscribeToEventType(eventType, a.wsConn) a.eventListeners[eventType] = []*EventListener{&evl} } } @@ -309,7 +309,7 @@ func (a *App) Start(ctx context.Context) { // subscribe to state_changed events id := internal.GetId() - ws.SubscribeToStateChangedEvents(id, a.wsWriter) + ws.SubscribeToStateChangedEvents(id, a.wsConn) a.entityListenersId = id // entity listeners runOnStartup diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 3d1605a..343ff7e 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -7,7 +7,7 @@ import ( /* Structs */ type AlarmControlPanel struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/climate.go b/internal/services/climate.go index c29f7ba..3050b6f 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -8,7 +8,7 @@ import ( /* Structs */ type Climate struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/cover.go b/internal/services/cover.go index f4f26ed..e0343ea 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Cover struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/event.go b/internal/services/event.go index ca9b522..5d4d061 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -6,7 +6,7 @@ import ( ) type Event struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } // Fire an event diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 75376dd..f298dd0 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -5,7 +5,7 @@ import ( ) type HomeAssistant struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } // TurnOn a Home Assistant entity. Takes an entityId and an optional diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 89811a9..187c0ff 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -7,7 +7,7 @@ import ( /* Structs */ type InputBoolean struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 8bfeb0a..41f4a74 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -7,7 +7,7 @@ import ( /* Structs */ type InputButton struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 06d1246..453abef 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -10,7 +10,7 @@ import ( /* Structs */ type InputDatetime struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/input_number.go b/internal/services/input_number.go index c82558e..cde8f09 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -7,7 +7,7 @@ import ( /* Structs */ type InputNumber struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/input_text.go b/internal/services/input_text.go index f28dd8c..23d5b41 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -7,7 +7,7 @@ import ( /* Structs */ type InputText struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/light.go b/internal/services/light.go index 74b317c..1a2f5cf 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Light struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/lock.go b/internal/services/lock.go index fc8b388..02b6755 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Lock struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/media_player.go b/internal/services/media_player.go index bcf1b14..0492cc4 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -7,7 +7,7 @@ import ( /* Structs */ type MediaPlayer struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/notify.go b/internal/services/notify.go index 8ed97da..a989892 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -6,7 +6,7 @@ import ( ) type Notify struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } // Send a notification. Takes a types.NotifyRequest. diff --git a/internal/services/number.go b/internal/services/number.go index bd04a2a..0074379 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Number struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/scene.go b/internal/services/scene.go index 87872b8..071c630 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Scene struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/script.go b/internal/services/script.go index 793f85b..f96d25a 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Script struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/services.go b/internal/services/services.go index dab7c57..175b064 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -27,7 +27,7 @@ func BuildService[ TTS | Vacuum | ZWaveJS, -](conn *ws.WebsocketWriter) *T { +](conn *ws.WebsocketConn) *T { return &T{conn: conn} } diff --git a/internal/services/switch.go b/internal/services/switch.go index 53ec144..7b0ea1f 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Switch struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/tts.go b/internal/services/tts.go index e4e5334..0e1c13e 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -7,7 +7,7 @@ import ( /* Structs */ type TTS struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index f6930cc..670dd16 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Vacuum struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 8b2fcf9..81d953f 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -7,7 +7,7 @@ import ( /* Structs */ type ZWaveJS struct { - conn *ws.WebsocketWriter + conn *ws.WebsocketConn } /* Public API */ diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index b559ae1..e173d90 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -24,12 +24,12 @@ type AuthMessage struct { AccessToken string `json:"access_token"` } -type WebsocketWriter struct { +type WebsocketConn struct { Conn *websocket.Conn mutex sync.Mutex } -func (w *WebsocketWriter) WriteMessage(msg interface{}) error { +func (w *WebsocketConn) WriteMessage(msg interface{}) error { w.mutex.Lock() defer w.mutex.Unlock() @@ -127,11 +127,11 @@ type SubEvent struct { EventType string `json:"event_type"` } -func SubscribeToStateChangedEvents(id int64, conn *WebsocketWriter) { +func SubscribeToStateChangedEvents(id int64, conn *WebsocketConn) { SubscribeToEventType("state_changed", conn, id) } -func SubscribeToEventType(eventType string, conn *WebsocketWriter, id ...int64) { +func SubscribeToEventType(eventType string, conn *WebsocketConn, id ...int64) { var finalId int64 if len(id) == 0 { finalId = internal.GetId() diff --git a/service.go b/service.go index 68ce1f1..42592be 100644 --- a/service.go +++ b/service.go @@ -30,7 +30,7 @@ type Service struct { ZWaveJS *services.ZWaveJS } -func newService(conn *ws.WebsocketWriter, httpClient *http.HttpClient) *Service { +func newService(conn *ws.WebsocketConn, httpClient *http.HttpClient) *Service { return &Service{ AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn), Climate: services.BuildService[services.Climate](conn), From 5aab2a645c434bec43b778900ca336572d38fe2b Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:13:15 +0200 Subject: [PATCH 018/103] WebsocketConn.Close(): new method --- app.go | 2 +- internal/websocket/websocket.go | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index 5676ec1..4842877 100644 --- a/app.go +++ b/app.go @@ -384,7 +384,7 @@ func (a *App) Close() { // called exactly once. func (a *App) close() { a.cancel() - a.conn.Close() + a.wsConn.Close() } func (a *App) GetService() *Service { diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index e173d90..4b4d31d 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -92,6 +92,10 @@ func ConnectionFromUri(ctx context.Context, uri, authToken string) (*websocket.C return conn, nil } +func (conn *WebsocketConn) Close() error { + return conn.Conn.Close() +} + func SendAuthMessage(conn *websocket.Conn, token string) error { err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token}) if err != nil { From 0a6c6f1c4d5cd5da8dfd025bb5fabf54e392ed44 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:15:32 +0200 Subject: [PATCH 019/103] WebsocketConn.WriteMessage(): change receiver 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 4b4d31d..75311ac 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -29,11 +29,11 @@ type WebsocketConn struct { mutex sync.Mutex } -func (w *WebsocketConn) WriteMessage(msg interface{}) error { - w.mutex.Lock() - defer w.mutex.Unlock() +func (conn *WebsocketConn) WriteMessage(msg interface{}) error { + conn.mutex.Lock() + defer conn.mutex.Unlock() - err := w.Conn.WriteJSON(msg) + err := conn.Conn.WriteJSON(msg) if err != nil { return err } From 4a31f802aa38c1ff0d1a1849dcd9635801bb2976 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:18:16 +0200 Subject: [PATCH 020/103] ListenWebsocket(): make a method of `WebsocketConn` --- app.go | 2 +- internal/websocket/reader.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 4842877..06ad779 100644 --- a/app.go +++ b/app.go @@ -343,7 +343,7 @@ func (a *App) Start(ctx context.Context) { // entity listeners and event listeners elChan := make(chan ws.ChanMsg) eg.Go(func() error { - ws.ListenWebsocket(a.conn, elChan) + a.wsConn.ListenWebsocket(elChan) cancel() return nil }) diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index 4d280a7..da226a7 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -3,8 +3,6 @@ package websocket import ( "encoding/json" "log/slog" - - "github.com/gorilla/websocket" ) type BaseMessage struct { @@ -23,9 +21,9 @@ type ChanMsg struct { // ListenWebsocket reads JSON-formatted messages from `conn`, partly // deserializes them, and sends them to `c`. If there is an error, it // closes `c` and returns. -func ListenWebsocket(conn *websocket.Conn, c chan<- ChanMsg) { +func (conn *WebsocketConn) ListenWebsocket(c chan<- ChanMsg) { for { - bytes, err := ReadMessage(conn) + bytes, err := ReadMessage(conn.Conn) if err != nil { slog.Error("Error reading from websocket:", err) close(c) From 8fc04def8fbce208b1760301218ea2a225187d8d Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:19:10 +0200 Subject: [PATCH 021/103] App.conn: remove member --- app.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app.go b/app.go index 06ad779..1a80e53 100644 --- a/app.go +++ b/app.go @@ -9,7 +9,6 @@ import ( "time" "github.com/golang-module/carbon" - "github.com/gorilla/websocket" sunriseLib "github.com/nathan-osman/go-sunrise" "golang.org/x/sync/errgroup" "saml.dev/gome-assistant/internal" @@ -24,8 +23,6 @@ var ErrInvalidToken = ws.ErrInvalidToken var ErrInvalidArgs = errors.New("invalid arguments provided") type App struct { - conn *websocket.Conn - // Wraps the ws connection with added mutex locking wsConn *ws.WebsocketConn @@ -116,7 +113,6 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { } return &App{ - conn: conn, wsConn: wsWriter, httpClient: httpClient, service: service, From b57a5c030e51661789c6e9969068a748153459ab Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:37:55 +0200 Subject: [PATCH 022/103] WebsocketConn: change how type is constructed In particular, don't make the caller pass in a `websocket.Conn`. --- app.go | 3 +- internal/websocket/websocket.go | 64 ++++++++++++++++----------------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git a/app.go b/app.go index 1a80e53..33705ba 100644 --- a/app.go +++ b/app.go @@ -98,14 +98,13 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { return nil, ErrInvalidArgs } - conn, err := ws.ConnectionFromUri(ctx, config.WebsocketURI, config.HAAuthToken) + wsWriter, err := ws.NewConnFromURI(ctx, config.WebsocketURI, config.HAAuthToken) if err != nil { return nil, err } httpClient := http.ClientFromUri(config.RESTBaseURI, config.HAAuthToken) - wsWriter := &ws.WebsocketConn{Conn: conn} service := newService(wsWriter, httpClient) state, err := newState(httpClient, config.HomeZoneEntityId) if err != nil { diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 75311ac..1f74d8d 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -29,37 +29,7 @@ type WebsocketConn struct { mutex sync.Mutex } -func (conn *WebsocketConn) WriteMessage(msg interface{}) error { - conn.mutex.Lock() - defer conn.mutex.Unlock() - - err := conn.Conn.WriteJSON(msg) - if err != nil { - return err - } - - return nil -} - -func ReadMessage(conn *websocket.Conn) ([]byte, error) { - _, msg, err := conn.ReadMessage() - if err != nil { - return []byte{}, err - } - return msg, nil -} - -func SetupConnection(ctx context.Context, ip, port, authToken string) (*websocket.Conn, error) { - uri := fmt.Sprintf("ws://%s:%s/api/websocket", ip, port) - return ConnectionFromUri(ctx, uri, authToken) -} - -func SetupSecureConnection(ctx context.Context, ip, port, authToken string) (*websocket.Conn, error) { - uri := fmt.Sprintf("wss://%s:%s/api/websocket", ip, port) - return ConnectionFromUri(ctx, uri, authToken) -} - -func ConnectionFromUri(ctx context.Context, uri, authToken string) (*websocket.Conn, error) { +func NewConnFromURI(ctx context.Context, uri string, authToken string) (*WebsocketConn, error) { // Init websocket connection dialer := websocket.DefaultDialer conn, _, err := dialer.DialContext(ctx, uri, nil) @@ -89,7 +59,37 @@ func ConnectionFromUri(ctx context.Context, uri, authToken string) (*websocket.C return nil, err } - return conn, nil + return &WebsocketConn{Conn: conn}, nil +} + +func NewConn(ctx context.Context, ip, port, authToken string) (*WebsocketConn, error) { + uri := fmt.Sprintf("ws://%s:%s/api/websocket", ip, port) + return NewConnFromURI(ctx, uri, authToken) +} + +func NewSecureConn(ctx context.Context, ip, port, authToken string) (*WebsocketConn, error) { + uri := fmt.Sprintf("wss://%s:%s/api/websocket", ip, port) + return NewConnFromURI(ctx, uri, authToken) +} + +func (conn *WebsocketConn) WriteMessage(msg interface{}) error { + conn.mutex.Lock() + defer conn.mutex.Unlock() + + err := conn.Conn.WriteJSON(msg) + if err != nil { + return err + } + + return nil +} + +func ReadMessage(conn *websocket.Conn) ([]byte, error) { + _, msg, err := conn.ReadMessage() + if err != nil { + return []byte{}, err + } + return msg, nil } func (conn *WebsocketConn) Close() error { From 6e24f42408aae085413b8d0ea6305dddf79875f0 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:39:05 +0200 Subject: [PATCH 023/103] WebsocketConn.conn: make member private --- internal/websocket/reader.go | 2 +- internal/websocket/websocket.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index da226a7..9e7635c 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -23,7 +23,7 @@ type ChanMsg struct { // closes `c` and returns. func (conn *WebsocketConn) ListenWebsocket(c chan<- ChanMsg) { for { - bytes, err := ReadMessage(conn.Conn) + bytes, err := ReadMessage(conn.conn) if err != nil { slog.Error("Error reading from websocket:", err) close(c) diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 1f74d8d..6337560 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -25,7 +25,7 @@ type AuthMessage struct { } type WebsocketConn struct { - Conn *websocket.Conn + conn *websocket.Conn mutex sync.Mutex } @@ -59,7 +59,7 @@ func NewConnFromURI(ctx context.Context, uri string, authToken string) (*Websock return nil, err } - return &WebsocketConn{Conn: conn}, nil + return &WebsocketConn{conn: conn}, nil } func NewConn(ctx context.Context, ip, port, authToken string) (*WebsocketConn, error) { @@ -76,7 +76,7 @@ func (conn *WebsocketConn) WriteMessage(msg interface{}) error { conn.mutex.Lock() defer conn.mutex.Unlock() - err := conn.Conn.WriteJSON(msg) + err := conn.conn.WriteJSON(msg) if err != nil { return err } @@ -93,7 +93,7 @@ func ReadMessage(conn *websocket.Conn) ([]byte, error) { } func (conn *WebsocketConn) Close() error { - return conn.Conn.Close() + return conn.conn.Close() } func SendAuthMessage(conn *websocket.Conn, token string) error { From ed26c0c816da97e5bba9eb0eff1790a961402714 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:43:09 +0200 Subject: [PATCH 024/103] websocket.Conn: rename type from `WebsocketConn` --- app.go | 2 +- internal/services/alarm_control_panel.go | 2 +- internal/services/climate.go | 2 +- internal/services/cover.go | 2 +- internal/services/event.go | 2 +- internal/services/homeassistant.go | 2 +- internal/services/input_boolean.go | 2 +- internal/services/input_button.go | 2 +- internal/services/input_datetime.go | 2 +- internal/services/input_number.go | 2 +- internal/services/input_text.go | 2 +- internal/services/light.go | 2 +- internal/services/lock.go | 2 +- internal/services/media_player.go | 2 +- internal/services/notify.go | 2 +- internal/services/number.go | 2 +- internal/services/scene.go | 2 +- internal/services/script.go | 2 +- internal/services/services.go | 2 +- internal/services/switch.go | 2 +- internal/services/tts.go | 2 +- internal/services/vacuum.go | 2 +- internal/services/zwavejs.go | 2 +- internal/websocket/reader.go | 2 +- internal/websocket/websocket.go | 18 +++++++++--------- service.go | 2 +- 26 files changed, 34 insertions(+), 34 deletions(-) diff --git a/app.go b/app.go index 33705ba..a14c92d 100644 --- a/app.go +++ b/app.go @@ -24,7 +24,7 @@ var ErrInvalidArgs = errors.New("invalid arguments provided") type App struct { // Wraps the ws connection with added mutex locking - wsConn *ws.WebsocketConn + wsConn *ws.Conn httpClient *http.HttpClient diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 343ff7e..a6e9b6b 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -7,7 +7,7 @@ import ( /* Structs */ type AlarmControlPanel struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/climate.go b/internal/services/climate.go index 3050b6f..1c7b6f3 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -8,7 +8,7 @@ import ( /* Structs */ type Climate struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/cover.go b/internal/services/cover.go index e0343ea..816bfc9 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Cover struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/event.go b/internal/services/event.go index 5d4d061..50a7913 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -6,7 +6,7 @@ import ( ) type Event struct { - conn *ws.WebsocketConn + conn *ws.Conn } // Fire an event diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index f298dd0..d2ed9b0 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -5,7 +5,7 @@ import ( ) type HomeAssistant struct { - conn *ws.WebsocketConn + conn *ws.Conn } // TurnOn a Home Assistant entity. Takes an entityId and an optional diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 187c0ff..43728dd 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -7,7 +7,7 @@ import ( /* Structs */ type InputBoolean struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 41f4a74..1fe099a 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -7,7 +7,7 @@ import ( /* Structs */ type InputButton struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 453abef..f715166 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -10,7 +10,7 @@ import ( /* Structs */ type InputDatetime struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/input_number.go b/internal/services/input_number.go index cde8f09..6bf19c5 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -7,7 +7,7 @@ import ( /* Structs */ type InputNumber struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 23d5b41..9fb7f4b 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -7,7 +7,7 @@ import ( /* Structs */ type InputText struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/light.go b/internal/services/light.go index 1a2f5cf..9026fc9 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Light struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/lock.go b/internal/services/lock.go index 02b6755..42c5db0 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Lock struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 0492cc4..af5ee8f 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -7,7 +7,7 @@ import ( /* Structs */ type MediaPlayer struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/notify.go b/internal/services/notify.go index a989892..9a6edc6 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -6,7 +6,7 @@ import ( ) type Notify struct { - conn *ws.WebsocketConn + conn *ws.Conn } // Send a notification. Takes a types.NotifyRequest. diff --git a/internal/services/number.go b/internal/services/number.go index 0074379..08dc0a1 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Number struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/scene.go b/internal/services/scene.go index 071c630..9a3f268 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Scene struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/script.go b/internal/services/script.go index f96d25a..2388a9d 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Script struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/services.go b/internal/services/services.go index 175b064..955fc3a 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -27,7 +27,7 @@ func BuildService[ TTS | Vacuum | ZWaveJS, -](conn *ws.WebsocketConn) *T { +](conn *ws.Conn) *T { return &T{conn: conn} } diff --git a/internal/services/switch.go b/internal/services/switch.go index 7b0ea1f..07802cd 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Switch struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/tts.go b/internal/services/tts.go index 0e1c13e..429f320 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -7,7 +7,7 @@ import ( /* Structs */ type TTS struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 670dd16..2a064b6 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -7,7 +7,7 @@ import ( /* Structs */ type Vacuum struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 81d953f..3025db7 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -7,7 +7,7 @@ import ( /* Structs */ type ZWaveJS struct { - conn *ws.WebsocketConn + conn *ws.Conn } /* Public API */ diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index 9e7635c..f14ba3e 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -21,7 +21,7 @@ type ChanMsg struct { // ListenWebsocket reads JSON-formatted messages from `conn`, partly // deserializes them, and sends them to `c`. If there is an error, it // closes `c` and returns. -func (conn *WebsocketConn) ListenWebsocket(c chan<- ChanMsg) { +func (conn *Conn) ListenWebsocket(c chan<- ChanMsg) { for { bytes, err := ReadMessage(conn.conn) if err != nil { diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 6337560..01ed581 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -24,12 +24,12 @@ type AuthMessage struct { AccessToken string `json:"access_token"` } -type WebsocketConn struct { +type Conn struct { conn *websocket.Conn mutex sync.Mutex } -func NewConnFromURI(ctx context.Context, uri string, authToken string) (*WebsocketConn, error) { +func NewConnFromURI(ctx context.Context, uri string, authToken string) (*Conn, error) { // Init websocket connection dialer := websocket.DefaultDialer conn, _, err := dialer.DialContext(ctx, uri, nil) @@ -59,20 +59,20 @@ func NewConnFromURI(ctx context.Context, uri string, authToken string) (*Websock return nil, err } - return &WebsocketConn{conn: conn}, nil + return &Conn{conn: conn}, nil } -func NewConn(ctx context.Context, ip, port, authToken string) (*WebsocketConn, error) { +func NewConn(ctx context.Context, ip, port, authToken string) (*Conn, error) { uri := fmt.Sprintf("ws://%s:%s/api/websocket", ip, port) return NewConnFromURI(ctx, uri, authToken) } -func NewSecureConn(ctx context.Context, ip, port, authToken string) (*WebsocketConn, error) { +func NewSecureConn(ctx context.Context, ip, port, authToken string) (*Conn, error) { uri := fmt.Sprintf("wss://%s:%s/api/websocket", ip, port) return NewConnFromURI(ctx, uri, authToken) } -func (conn *WebsocketConn) WriteMessage(msg interface{}) error { +func (conn *Conn) WriteMessage(msg interface{}) error { conn.mutex.Lock() defer conn.mutex.Unlock() @@ -92,7 +92,7 @@ func ReadMessage(conn *websocket.Conn) ([]byte, error) { return msg, nil } -func (conn *WebsocketConn) Close() error { +func (conn *Conn) Close() error { return conn.conn.Close() } @@ -131,11 +131,11 @@ type SubEvent struct { EventType string `json:"event_type"` } -func SubscribeToStateChangedEvents(id int64, conn *WebsocketConn) { +func SubscribeToStateChangedEvents(id int64, conn *Conn) { SubscribeToEventType("state_changed", conn, id) } -func SubscribeToEventType(eventType string, conn *WebsocketConn, id ...int64) { +func SubscribeToEventType(eventType string, conn *Conn, id ...int64) { var finalId int64 if len(id) == 0 { finalId = internal.GetId() diff --git a/service.go b/service.go index 42592be..34059d4 100644 --- a/service.go +++ b/service.go @@ -30,7 +30,7 @@ type Service struct { ZWaveJS *services.ZWaveJS } -func newService(conn *ws.WebsocketConn, httpClient *http.HttpClient) *Service { +func newService(conn *ws.Conn, httpClient *http.HttpClient) *Service { return &Service{ AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn), Climate: services.BuildService[services.Climate](conn), From a30ad1244aa5b5c3360dd8b86390ccb0a5fb985a Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:47:32 +0200 Subject: [PATCH 025/103] Remove some more abbreviations --- app.go | 21 +++++++++++---------- eventListener.go | 5 +++-- internal/services/alarm_control_panel.go | 4 ++-- internal/services/climate.go | 4 ++-- internal/services/cover.go | 4 ++-- internal/services/event.go | 4 ++-- internal/services/homeassistant.go | 4 ++-- internal/services/input_boolean.go | 4 ++-- internal/services/input_button.go | 4 ++-- internal/services/input_datetime.go | 4 ++-- internal/services/input_number.go | 4 ++-- internal/services/input_text.go | 4 ++-- internal/services/light.go | 4 ++-- internal/services/lock.go | 4 ++-- internal/services/media_player.go | 4 ++-- internal/services/notify.go | 4 ++-- internal/services/number.go | 4 ++-- internal/services/scene.go | 4 ++-- internal/services/script.go | 4 ++-- internal/services/services.go | 4 ++-- internal/services/switch.go | 4 ++-- internal/services/tts.go | 4 ++-- internal/services/vacuum.go | 4 ++-- internal/services/zwavejs.go | 4 ++-- service.go | 4 ++-- 25 files changed, 60 insertions(+), 58 deletions(-) diff --git a/app.go b/app.go index a14c92d..9d1f941 100644 --- a/app.go +++ b/app.go @@ -11,27 +11,28 @@ import ( "github.com/golang-module/carbon" sunriseLib "github.com/nathan-osman/go-sunrise" "golang.org/x/sync/errgroup" + "saml.dev/gome-assistant/internal" "saml.dev/gome-assistant/internal/http" - pq "saml.dev/gome-assistant/internal/priorityqueue" - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/priorityqueue" + "saml.dev/gome-assistant/internal/websocket" ) // Returned by NewApp() if authentication fails -var ErrInvalidToken = ws.ErrInvalidToken +var ErrInvalidToken = websocket.ErrInvalidToken var ErrInvalidArgs = errors.New("invalid arguments provided") type App struct { // Wraps the ws connection with added mutex locking - wsConn *ws.Conn + wsConn *websocket.Conn httpClient *http.HttpClient service *Service state *StateImpl - scheduledActions pq.PriorityQueue + scheduledActions priorityqueue.PriorityQueue entityListeners map[string][]*EntityListener entityListenersId int64 eventListeners map[string][]*EventListener @@ -98,7 +99,7 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { return nil, ErrInvalidArgs } - wsWriter, err := ws.NewConnFromURI(ctx, config.WebsocketURI, config.HAAuthToken) + wsWriter, err := websocket.NewConnFromURI(ctx, config.WebsocketURI, config.HAAuthToken) if err != nil { return nil, err } @@ -116,7 +117,7 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { httpClient: httpClient, service: service, state: state, - scheduledActions: pq.New(), + scheduledActions: priorityqueue.New(), entityListeners: map[string][]*EntityListener{}, eventListeners: map[string][]*EventListener{}, cancel: func() {}, @@ -234,7 +235,7 @@ func (a *App) RegisterEventListeners(evls ...EventListener) { if elList, ok := a.eventListeners[eventType]; ok { a.eventListeners[eventType] = append(elList, &evl) } else { - ws.SubscribeToEventType(eventType, a.wsConn) + websocket.SubscribeToEventType(eventType, a.wsConn) a.eventListeners[eventType] = []*EventListener{&evl} } } @@ -304,7 +305,7 @@ func (a *App) Start(ctx context.Context) { // subscribe to state_changed events id := internal.GetId() - ws.SubscribeToStateChangedEvents(id, a.wsConn) + websocket.SubscribeToStateChangedEvents(id, a.wsConn) a.entityListenersId = id // entity listeners runOnStartup @@ -336,7 +337,7 @@ func (a *App) Start(ctx context.Context) { } // entity listeners and event listeners - elChan := make(chan ws.ChanMsg) + elChan := make(chan websocket.ChanMsg) eg.Go(func() error { a.wsConn.ListenWebsocket(elChan) cancel() diff --git a/eventListener.go b/eventListener.go index 37fe98a..fa68ec7 100644 --- a/eventListener.go +++ b/eventListener.go @@ -6,8 +6,9 @@ import ( "time" "github.com/golang-module/carbon" + "saml.dev/gome-assistant/internal" - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) type EventListener struct { @@ -139,7 +140,7 @@ type BaseEventMsg struct { } /* Functions */ -func callEventListeners(app *App, msg ws.ChanMsg) { +func callEventListeners(app *App, msg websocket.ChanMsg) { baseEventMsg := BaseEventMsg{} json.Unmarshal(msg.Raw, &baseEventMsg) listeners, ok := app.eventListeners[baseEventMsg.Event.EventType] diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index a6e9b6b..d7a6c90 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type AlarmControlPanel struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/climate.go b/internal/services/climate.go index 1c7b6f3..7a6e861 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -1,14 +1,14 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" "saml.dev/gome-assistant/types" ) /* Structs */ type Climate struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/cover.go b/internal/services/cover.go index 816bfc9..537d31b 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Cover struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/event.go b/internal/services/event.go index 50a7913..acec7c2 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -2,11 +2,11 @@ package services import ( "saml.dev/gome-assistant/internal" - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) type Event struct { - conn *ws.Conn + conn *websocket.Conn } // Fire an event diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index d2ed9b0..b743033 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -1,11 +1,11 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) type HomeAssistant struct { - conn *ws.Conn + conn *websocket.Conn } // TurnOn a Home Assistant entity. Takes an entityId and an optional diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 43728dd..345662e 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputBoolean struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 1fe099a..8e881c1 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputButton struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index f715166..ffd6344 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -4,13 +4,13 @@ import ( "fmt" "time" - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputDatetime struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 6bf19c5..55197f3 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputNumber struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 9fb7f4b..5f83f92 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputText struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/light.go b/internal/services/light.go index 9026fc9..313f97c 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Light struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/lock.go b/internal/services/lock.go index 42c5db0..9e8d780 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Lock struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/media_player.go b/internal/services/media_player.go index af5ee8f..74ad896 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type MediaPlayer struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/notify.go b/internal/services/notify.go index 9a6edc6..ab134cd 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -1,12 +1,12 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" "saml.dev/gome-assistant/types" ) type Notify struct { - conn *ws.Conn + conn *websocket.Conn } // Send a notification. Takes a types.NotifyRequest. diff --git a/internal/services/number.go b/internal/services/number.go index 08dc0a1..cc3916b 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Number struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/scene.go b/internal/services/scene.go index 9a3f268..2d2b34a 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Scene struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/script.go b/internal/services/script.go index 2388a9d..f203679 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Script struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/services.go b/internal/services/services.go index 955fc3a..7297ece 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -2,7 +2,7 @@ package services import ( "saml.dev/gome-assistant/internal" - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) func BuildService[ @@ -27,7 +27,7 @@ func BuildService[ TTS | Vacuum | ZWaveJS, -](conn *ws.Conn) *T { +](conn *websocket.Conn) *T { return &T{conn: conn} } diff --git a/internal/services/switch.go b/internal/services/switch.go index 07802cd..49de778 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Switch struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/tts.go b/internal/services/tts.go index 429f320..6f765bd 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type TTS struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 2a064b6..e34ba73 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Vacuum struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 3025db7..d9aae49 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -1,13 +1,13 @@ package services import ( - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type ZWaveJS struct { - conn *ws.Conn + conn *websocket.Conn } /* Public API */ diff --git a/service.go b/service.go index 34059d4..541ec5b 100644 --- a/service.go +++ b/service.go @@ -3,7 +3,7 @@ package gomeassistant import ( "saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/services" - ws "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/internal/websocket" ) type Service struct { @@ -30,7 +30,7 @@ type Service struct { ZWaveJS *services.ZWaveJS } -func newService(conn *ws.Conn, httpClient *http.HttpClient) *Service { +func newService(conn *websocket.Conn, httpClient *http.HttpClient) *Service { return &Service{ AlarmControlPanel: services.BuildService[services.AlarmControlPanel](conn), Climate: services.BuildService[services.Climate](conn), From 94ccdc4726d4fc2f3b20c06911d9a70f91e94231 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 21:58:59 +0200 Subject: [PATCH 026/103] websocket.Conn: add some methods Instead of using public functions, make these into private methods. --- internal/websocket/reader.go | 2 +- internal/websocket/websocket.go | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index f14ba3e..db130bd 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -23,7 +23,7 @@ type ChanMsg struct { // closes `c` and returns. func (conn *Conn) ListenWebsocket(c chan<- ChanMsg) { for { - bytes, err := ReadMessage(conn.conn) + bytes, err := conn.readMessage() if err != nil { slog.Error("Error reading from websocket:", err) close(c) diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 01ed581..f551fc3 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -32,34 +32,35 @@ type Conn struct { func NewConnFromURI(ctx context.Context, uri string, authToken string) (*Conn, error) { // Init websocket connection dialer := websocket.DefaultDialer - conn, _, err := dialer.DialContext(ctx, uri, nil) + wsConn, _, err := dialer.DialContext(ctx, uri, nil) if err != nil { slog.Error("Failed to connect to websocket. Check URI\n", "uri", uri) return nil, err } + conn := &Conn{conn: wsConn} + // Read auth_required message - _, err = ReadMessage(conn) - if err != nil { + if _, err := conn.readMessage(); err != nil { slog.Error("Unknown error creating websocket client\n") return nil, err } // Send auth message - err = SendAuthMessage(conn, authToken) + err = conn.sendAuthMessage(authToken) if err != nil { slog.Error("Unknown error creating websocket client\n") return nil, err } // Verify auth message was successful - err = VerifyAuthResponse(conn) + err = conn.verifyAuthResponse() if err != nil { slog.Error("Auth token is invalid. Please double check it or create a new token in your Home Assistant profile\n") return nil, err } - return &Conn{conn: conn}, nil + return conn, nil } func NewConn(ctx context.Context, ip, port, authToken string) (*Conn, error) { @@ -84,8 +85,8 @@ func (conn *Conn) WriteMessage(msg interface{}) error { return nil } -func ReadMessage(conn *websocket.Conn) ([]byte, error) { - _, msg, err := conn.ReadMessage() +func (conn *Conn) readMessage() ([]byte, error) { + _, msg, err := conn.conn.ReadMessage() if err != nil { return []byte{}, err } @@ -96,8 +97,8 @@ func (conn *Conn) Close() error { return conn.conn.Close() } -func SendAuthMessage(conn *websocket.Conn, token string) error { - err := conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token}) +func (conn *Conn) sendAuthMessage(token string) error { + err := conn.conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token}) if err != nil { return err } @@ -109,8 +110,8 @@ type authResponse struct { Message string `json:"message"` } -func VerifyAuthResponse(conn *websocket.Conn) error { - msg, err := ReadMessage(conn) +func (conn *Conn) verifyAuthResponse() error { + msg, err := conn.readMessage() if err != nil { return err } From f866c52422c7dc62a060ec97ccf13bd5d5f81d84 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 22:01:28 +0200 Subject: [PATCH 027/103] websocket.Conn.Start(): rename method from `ListenWebsocket()` --- app.go | 2 +- internal/websocket/reader.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 9d1f941..525fa72 100644 --- a/app.go +++ b/app.go @@ -339,7 +339,7 @@ func (a *App) Start(ctx context.Context) { // entity listeners and event listeners elChan := make(chan websocket.ChanMsg) eg.Go(func() error { - a.wsConn.ListenWebsocket(elChan) + a.wsConn.Start(elChan) cancel() return nil }) diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index db130bd..f581144 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -18,10 +18,10 @@ type ChanMsg struct { Raw []byte } -// ListenWebsocket reads JSON-formatted messages from `conn`, partly -// deserializes them, and sends them to `c`. If there is an error, it -// closes `c` and returns. -func (conn *Conn) ListenWebsocket(c chan<- ChanMsg) { +// Start reads JSON-formatted messages from `conn`, partly +// deserializes them, and sends them to `c` for processing. If there +// is an error reading from `conn`, close `c` and return. +func (conn *Conn) Start(c chan<- ChanMsg) { for { bytes, err := conn.readMessage() if err != nil { From 0fd02f80371a26c8820fcc406f2691b6816f2c69 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 22:05:34 +0200 Subject: [PATCH 028/103] websocket.Conn: add some more methods Make the subscribe functions into methods of `websocket.Conn`. --- app.go | 4 ++-- internal/websocket/websocket.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 525fa72..68f5116 100644 --- a/app.go +++ b/app.go @@ -235,7 +235,7 @@ func (a *App) RegisterEventListeners(evls ...EventListener) { if elList, ok := a.eventListeners[eventType]; ok { a.eventListeners[eventType] = append(elList, &evl) } else { - websocket.SubscribeToEventType(eventType, a.wsConn) + a.wsConn.SubscribeToEventType(eventType) a.eventListeners[eventType] = []*EventListener{&evl} } } @@ -305,7 +305,7 @@ func (a *App) Start(ctx context.Context) { // subscribe to state_changed events id := internal.GetId() - websocket.SubscribeToStateChangedEvents(id, a.wsConn) + a.wsConn.SubscribeToStateChangedEvents(id) a.entityListenersId = id // entity listeners runOnStartup diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index f551fc3..7871d04 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -132,11 +132,11 @@ type SubEvent struct { EventType string `json:"event_type"` } -func SubscribeToStateChangedEvents(id int64, conn *Conn) { - SubscribeToEventType("state_changed", conn, id) +func (conn *Conn) SubscribeToStateChangedEvents(id int64) { + conn.SubscribeToEventType("state_changed", id) } -func SubscribeToEventType(eventType string, conn *Conn, id ...int64) { +func (conn *Conn) SubscribeToEventType(eventType string, id ...int64) { var finalId int64 if len(id) == 0 { finalId = internal.GetId() From 059e346d3ad1188b245cddd84ee57c028b1d3395 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 23:00:48 +0200 Subject: [PATCH 029/103] websocket.Conn: introduce a way to subscribe to IDs --- internal/websocket/reader.go | 72 +++++++++++++++++++++++++++++++-- internal/websocket/websocket.go | 39 +++++++++++++++--- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index f581144..753f008 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -2,6 +2,7 @@ package websocket import ( "encoding/json" + "fmt" "log/slog" ) @@ -18,9 +19,70 @@ type ChanMsg struct { Raw []byte } +// subscribe creates a new (unique) subscription number and subscribes +// `subscriber` to it. +func (conn *Conn) subscribe(subscriber Subscriber) Subscription { + conn.subscribeMutex.Lock() + defer conn.subscribeMutex.Unlock() + + conn.lastID++ + id := conn.lastID + conn.subscribers[id] = subscriber + return Subscription{ + conn: conn, + id: conn.lastID, + } +} + +// unsubscribe unsubscribes from `subscription`. It must be called +// exactly once for each subscription. +func (conn *Conn) unsubscribe(id int64) error { + conn.subscribeMutex.Lock() + defer conn.subscribeMutex.Unlock() + + if _, ok := conn.subscribers[id]; !ok { + return fmt.Errorf("subscription ID %d wasn't active", id) + } + delete(conn.subscribers, id) + return nil +} + +func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { + conn.subscribeMutex.RLock() + defer conn.subscribeMutex.RUnlock() + + subscriber, ok := conn.subscribers[id] + return subscriber, ok +} + +// WatchEvents subscribes to events of the given type, invoking +// `subscriber` when any such events are received. Calls to +// `subscriber` are synchronous with respect to any other received +// messages, but asynchronous with respect to writes. +func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscription, error) { + // Make sure we're listening before events might start arriving: + subscription := conn.subscribe(subscriber) + + e := SubEvent{ + Id: subscription.ID(), + Type: "subscribe_events", + EventType: eventType, + } + err := conn.WriteMessage(e) + if err != nil { + conn.unsubscribe(subscription.ID()) + return Subscription{}, fmt.Errorf("error writing to websocket: %w", err) + } + // m, _ := ReadMessage(conn, ctx) + // log.Default().Println(string(m)) + return subscription, nil +} + // Start reads JSON-formatted messages from `conn`, partly -// deserializes them, and sends them to `c` for processing. If there -// is an error reading from `conn`, close `c` and return. +// deserializes them, and processes them. If the message ID is +// currently subscribed to, invoke the subscriber for the message. +// Otherwise, send them to `c`. If there is an error reading from +// `conn`, close `c` and return. func (conn *Conn) Start(c chan<- ChanMsg) { for { bytes, err := conn.readMessage() @@ -45,6 +107,10 @@ func (conn *Conn) Start(c chan<- ChanMsg) { Raw: bytes, } - c <- chanMsg + if subscriber, ok := conn.getSubscriber(chanMsg.Id); ok { + subscriber(chanMsg) + } else { + c <- chanMsg + } } } diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 7871d04..bf587df 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -25,8 +25,34 @@ type AuthMessage struct { } type Conn struct { - conn *websocket.Conn - mutex sync.Mutex + writeMutex sync.Mutex + conn *websocket.Conn + + subscribeMutex sync.RWMutex + // lastID is the last message ID that has already been used. + lastID int64 + subscribers map[int64]Subscriber +} + +// Subscriber is called synchronously when a message with the +// subscribed `id` is received. +type Subscriber func(msg ChanMsg) + +type Subscription struct { + conn *Conn + id int64 +} + +func (subscription Subscription) ID() int64 { + return subscription.id +} + +func (subscription *Subscription) Cancel() { + if subscription.id == 0 { + return + } + subscription.conn.unsubscribe(subscription.id) + subscription.id = 0 } func NewConnFromURI(ctx context.Context, uri string, authToken string) (*Conn, error) { @@ -38,7 +64,10 @@ func NewConnFromURI(ctx context.Context, uri string, authToken string) (*Conn, e return nil, err } - conn := &Conn{conn: wsConn} + conn := &Conn{ + conn: wsConn, + subscribers: make(map[int64]Subscriber), + } // Read auth_required message if _, err := conn.readMessage(); err != nil { @@ -74,8 +103,8 @@ func NewSecureConn(ctx context.Context, ip, port, authToken string) (*Conn, erro } func (conn *Conn) WriteMessage(msg interface{}) error { - conn.mutex.Lock() - defer conn.mutex.Unlock() + conn.writeMutex.Lock() + defer conn.writeMutex.Unlock() err := conn.conn.WriteJSON(msg) if err != nil { From 7bc5ce68c013d28df2e74db2e5b630528605edcf Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 13 Apr 2024 23:10:58 +0200 Subject: [PATCH 030/103] Subscribe to state changed listeners through the new mechanism --- app.go | 31 +++++++++++++++++-------------- internal/websocket/reader.go | 4 ++++ internal/websocket/websocket.go | 7 +++---- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app.go b/app.go index 68f5116..b949ff1 100644 --- a/app.go +++ b/app.go @@ -12,7 +12,6 @@ import ( sunriseLib "github.com/nathan-osman/go-sunrise" "golang.org/x/sync/errgroup" - "saml.dev/gome-assistant/internal" "saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/priorityqueue" "saml.dev/gome-assistant/internal/websocket" @@ -32,10 +31,9 @@ type App struct { service *Service state *StateImpl - scheduledActions priorityqueue.PriorityQueue - entityListeners map[string][]*EntityListener - entityListenersId int64 - eventListeners map[string][]*EventListener + scheduledActions priorityqueue.PriorityQueue + entityListeners map[string][]*EntityListener + eventListeners map[string][]*EventListener // If `App.Start()` has been called, `cancel()` cancels the // context being used, which causes the app to shut down cleanly. @@ -287,7 +285,7 @@ func getNextSunRiseOrSet(a *App, sunrise bool, offset ...DurationString) carbon. // Start the app. When `ctx` expires, the app closes the connection // and returns. -func (a *App) Start(ctx context.Context) { +func (a *App) Start(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) a.cancel = cancel defer cancel() @@ -304,9 +302,16 @@ func (a *App) Start(ctx context.Context) { }) // subscribe to state_changed events - id := internal.GetId() - a.wsConn.SubscribeToStateChangedEvents(id) - a.entityListenersId = id + stateChangedSubscription, err := a.wsConn.WatchStateChangedEvents( + func(msg websocket.ChanMsg) { + go callEntityListeners(a, msg.Raw) + }, + ) + if err != nil { + return fmt.Errorf("subscribing to 'state_changed' events: %w", err) + } + + defer stateChangedSubscription.Cancel() // entity listeners runOnStartup for eid, etls := range a.entityListeners { @@ -350,11 +355,7 @@ func (a *App) Start(ctx context.Context) { if !ok { break } - if a.entityListenersId == msg.Id { - go callEntityListeners(a, msg.Raw) - } else { - go callEventListeners(a, msg) - } + go callEventListeners(a, msg) } return nil }) @@ -366,6 +367,8 @@ func (a *App) Start(ctx context.Context) { }) eg.Wait() + + return nil } // Close closes the connection and releases any resources. It may be diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index 753f008..a3013c3 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -78,6 +78,10 @@ func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscrip return subscription, nil } +func (conn *Conn) WatchStateChangedEvents(subscriber Subscriber) (Subscription, error) { + return conn.WatchEvents("state_changed", subscriber) +} + // Start reads JSON-formatted messages from `conn`, partly // deserializes them, and processes them. If the message ID is // currently subscribed to, invoke the subscriber for the message. diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index bf587df..0ee2bdc 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -51,6 +51,9 @@ func (subscription *Subscription) Cancel() { if subscription.id == 0 { return } + + // FIXME: this should also unsubscribe at the server. + subscription.conn.unsubscribe(subscription.id) subscription.id = 0 } @@ -161,10 +164,6 @@ type SubEvent struct { EventType string `json:"event_type"` } -func (conn *Conn) SubscribeToStateChangedEvents(id int64) { - conn.SubscribeToEventType("state_changed", id) -} - func (conn *Conn) SubscribeToEventType(eventType string, id ...int64) { var finalId int64 if len(id) == 0 { From 309efa95994da1f58efcf7d737664af84ff0684d Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 00:01:50 +0200 Subject: [PATCH 031/103] More work on event handling --- app.go | 11 +++++------ entitylistener.go | 4 +++- eventListener.go | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app.go b/app.go index b949ff1..ff15723 100644 --- a/app.go +++ b/app.go @@ -230,12 +230,11 @@ func (a *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) - } else { + elList, ok := a.eventListeners[eventType] + if !ok { a.wsConn.SubscribeToEventType(eventType) - a.eventListeners[eventType] = []*EventListener{&evl} } + a.eventListeners[eventType] = append(elList, &evl) } } } @@ -304,7 +303,7 @@ func (a *App) Start(ctx context.Context) error { // subscribe to state_changed events stateChangedSubscription, err := a.wsConn.WatchStateChangedEvents( func(msg websocket.ChanMsg) { - go callEntityListeners(a, msg.Raw) + go a.callEntityListeners(msg) }, ) if err != nil { @@ -355,7 +354,7 @@ func (a *App) Start(ctx context.Context) error { if !ok { break } - go callEventListeners(a, msg) + go a.callEventListeners(msg) } return nil }) diff --git a/entitylistener.go b/entitylistener.go index 2dd3a3a..9272327 100644 --- a/entitylistener.go +++ b/entitylistener.go @@ -7,6 +7,7 @@ import ( "github.com/golang-module/carbon" "saml.dev/gome-assistant/internal" + "saml.dev/gome-assistant/internal/websocket" ) type EntityListener struct { @@ -191,7 +192,8 @@ func (b elBuilder3) Build() EntityListener { } /* Functions */ -func callEntityListeners(app *App, msgBytes []byte) { +func (app *App) callEntityListeners(chanMsg websocket.ChanMsg) { + msgBytes := chanMsg.Raw msg := stateChangedMsg{} json.Unmarshal(msgBytes, &msg) data := msg.Event.Data diff --git a/eventListener.go b/eventListener.go index fa68ec7..b74e323 100644 --- a/eventListener.go +++ b/eventListener.go @@ -140,7 +140,7 @@ type BaseEventMsg struct { } /* Functions */ -func callEventListeners(app *App, msg websocket.ChanMsg) { +func (app *App) callEventListeners(msg websocket.ChanMsg) { baseEventMsg := BaseEventMsg{} json.Unmarshal(msg.Raw, &baseEventMsg) listeners, ok := app.eventListeners[baseEventMsg.Event.EventType] From 84b07550e2345f66370876fd32698f8613793c73 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 00:10:33 +0200 Subject: [PATCH 032/103] Subscribe to event listeners through the new mechanism --- app.go | 27 +++++++++++++-------------- internal/websocket/reader.go | 10 +++------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/app.go b/app.go index ff15723..2e8eba5 100644 --- a/app.go +++ b/app.go @@ -232,7 +232,18 @@ func (a *App) RegisterEventListeners(evls ...EventListener) { for _, eventType := range evl.eventTypes { elList, ok := a.eventListeners[eventType] if !ok { - a.wsConn.SubscribeToEventType(eventType) + // FIXME: keep track of subscriptions so that they can + // be unsubscribed from. + _, err := a.wsConn.WatchEvents( + eventType, + func(msg websocket.ChanMsg) { + go a.callEventListeners(msg) + }, + ) + if err != nil { + // FIXME: better error handling + panic(err) + } } a.eventListeners[eventType] = append(elList, &evl) } @@ -341,24 +352,12 @@ func (a *App) Start(ctx context.Context) error { } // entity listeners and event listeners - elChan := make(chan websocket.ChanMsg) eg.Go(func() error { - a.wsConn.Start(elChan) + a.wsConn.Start() cancel() return nil }) - eg.Go(func() error { - for { - msg, ok := <-elChan - if !ok { - break - } - go a.callEventListeners(msg) - } - return nil - }) - eg.Go(func() error { <-ctx.Done() a.Close() diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index a3013c3..d30345a 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -84,15 +84,13 @@ func (conn *Conn) WatchStateChangedEvents(subscriber Subscriber) (Subscription, // Start reads JSON-formatted messages from `conn`, partly // deserializes them, and processes them. If the message ID is -// currently subscribed to, invoke the subscriber for the message. -// Otherwise, send them to `c`. If there is an error reading from -// `conn`, close `c` and return. -func (conn *Conn) Start(c chan<- ChanMsg) { +// currently subscribed to, invoke the subscriber for the message. If +// there is an error reading from `conn`, log it and return. +func (conn *Conn) Start() { for { bytes, err := conn.readMessage() if err != nil { slog.Error("Error reading from websocket:", err) - close(c) return } @@ -113,8 +111,6 @@ func (conn *Conn) Start(c chan<- ChanMsg) { if subscriber, ok := conn.getSubscriber(chanMsg.Id); ok { subscriber(chanMsg) - } else { - c <- chanMsg } } } From ef130ce6aec92de17131cc516785cd8354dca2f8 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 00:19:33 +0200 Subject: [PATCH 033/103] Rename local `*App` variables to `app` --- app.go | 114 ++++++++++++++++++++++++++-------------------------- interval.go | 14 +++---- schedule.go | 20 ++++----- 3 files changed, 74 insertions(+), 74 deletions(-) diff --git a/app.go b/app.go index 2e8eba5..a6c016b 100644 --- a/app.go +++ b/app.go @@ -184,31 +184,31 @@ func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { type scheduledAction interface { String() string Hash() string - initializeNextRunTime(a *App) - shouldRun(a *App) bool - run(a *App) - updateNextRunTime(a *App) + initializeNextRunTime(app *App) + shouldRun(app *App) bool + run(app *App) + updateNextRunTime(app *App) getNextRunTime() time.Time } -func (a *App) RegisterScheduledAction(action scheduledAction) { - action.initializeNextRunTime(a) - a.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) +func (app *App) RegisterScheduledAction(action scheduledAction) { + action.initializeNextRunTime(app) + app.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) } -func (a *App) RegisterSchedules(schedules ...*DailySchedule) { +func (app *App) RegisterSchedules(schedules ...*DailySchedule) { for _, s := range schedules { - a.RegisterScheduledAction(s) + app.RegisterScheduledAction(s) } } -func (a *App) RegisterIntervals(intervals ...*Interval) { +func (app *App) RegisterIntervals(intervals ...*Interval) { for _, i := range intervals { - a.RegisterScheduledAction(i) + app.RegisterScheduledAction(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 == "" { @@ -217,27 +217,27 @@ 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 { - elList, ok := a.eventListeners[eventType] + elList, ok := app.eventListeners[eventType] if !ok { // FIXME: keep track of subscriptions so that they can // be unsubscribed from. - _, err := a.wsConn.WatchEvents( + _, err := app.wsConn.WatchEvents( eventType, func(msg websocket.ChanMsg) { - go a.callEventListeners(msg) + go app.callEventListeners(msg) }, ) if err != nil { @@ -245,7 +245,7 @@ func (a *App) RegisterEventListeners(evls ...EventListener) { panic(err) } } - a.eventListeners[eventType] = append(elList, &evl) + app.eventListeners[eventType] = append(elList, &evl) } } } @@ -283,38 +283,38 @@ 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 } // Start the app. When `ctx` expires, the app closes the connection // and returns. -func (a *App) Start(ctx context.Context) error { +func (app *App) Start(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) - a.cancel = cancel + app.cancel = cancel defer cancel() eg, ctx := errgroup.WithContext(ctx) - slog.Info("Starting", "scheduled actions", a.scheduledActions.Len()) - slog.Info("Starting", "entity listeners", len(a.entityListeners)) - slog.Info("Starting", "event listeners", len(a.eventListeners)) + slog.Info("Starting", "scheduled actions", app.scheduledActions.Len()) + slog.Info("Starting", "entity listeners", len(app.entityListeners)) + slog.Info("Starting", "event listeners", len(app.eventListeners)) eg.Go(func() error { - a.runScheduledActions(ctx) + app.runScheduledActions(ctx) return nil }) // subscribe to state_changed events - stateChangedSubscription, err := a.wsConn.WatchStateChangedEvents( + stateChangedSubscription, err := app.wsConn.WatchStateChangedEvents( func(msg websocket.ChanMsg) { - go a.callEntityListeners(msg) + go app.callEntityListeners(msg) }, ) if err != nil { @@ -324,12 +324,12 @@ func (a *App) Start(ctx context.Context) error { defer stateChangedSubscription.Cancel() // 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") } @@ -337,7 +337,7 @@ func (a *App) Start(ctx context.Context) error { etl.runOnStartupCompleted = true etl := etl eg.Go(func() error { - etl.callback(a.service, a.state, EntityData{ + etl.callback(app.service, app.state, EntityData{ TriggerEntityId: eid, FromState: entityState.State, FromAttributes: entityState.Attributes, @@ -353,14 +353,14 @@ func (a *App) Start(ctx context.Context) error { // entity listeners and event listeners eg.Go(func() error { - a.wsConn.Start() + app.wsConn.Start() cancel() return nil }) eg.Go(func() error { <-ctx.Done() - a.Close() + app.Close() return nil }) @@ -371,29 +371,29 @@ func (a *App) Start(ctx context.Context) error { // Close closes the connection and releases any resources. It may be // called more than once; only the first call does anything. -func (a *App) Close() { - a.closeOnce.Do(func() { - a.close() +func (app *App) Close() { + app.closeOnce.Do(func() { + app.close() }) } // close closes the connection and releases resources. It must be // called exactly once. -func (a *App) close() { - a.cancel() - a.wsConn.Close() +func (app *App) close() { + app.cancel() + app.wsConn.Close() } -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 } -func (a *App) runScheduledActions(ctx context.Context) { - if a.scheduledActions.Len() == 0 { +func (app *App) runScheduledActions(ctx context.Context) { + if app.scheduledActions.Len() == 0 { return } @@ -404,7 +404,7 @@ func (a *App) runScheduledActions(ctx context.Context) { } for { - action := a.popScheduledAction() + action := app.popScheduledAction() if action.getNextRunTime().After(time.Now()) { timer.Reset(time.Until(action.getNextRunTime())) @@ -415,20 +415,20 @@ func (a *App) runScheduledActions(ctx context.Context) { } } - if action.shouldRun(a) { - go action.run(a) + if action.shouldRun(app) { + go action.run(app) } - a.requeueScheduledAction(action) + app.requeueScheduledAction(action) } } -func (a *App) popScheduledAction() scheduledAction { - action, _ := a.scheduledActions.Pop() +func (app *App) popScheduledAction() scheduledAction { + action, _ := app.scheduledActions.Pop() return action.(scheduledAction) } -func (a *App) requeueScheduledAction(action scheduledAction) { - action.updateNextRunTime(a) - a.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) +func (app *App) requeueScheduledAction(action scheduledAction) { + action.updateNextRunTime(app) + app.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) } diff --git a/interval.go b/interval.go index 4635ac9..c983cad 100644 --- a/interval.go +++ b/interval.go @@ -145,7 +145,7 @@ func (sb intervalBuilderEnd) Build() *Interval { return sb.interval } -func (i *Interval) initializeNextRunTime(a *App) { +func (i *Interval) initializeNextRunTime(app *App) { if i.frequency == 0 { slog.Error("A schedule must use either set frequency via Every()") panic(ErrInvalidArgs) @@ -162,7 +162,7 @@ func (i *Interval) getNextRunTime() time.Time { return i.nextRunTime } -func (i Interval) shouldRun(a *App) bool { +func (i Interval) shouldRun(app *App) bool { if c := checkStartEndTime(i.startTime /* isStart = */, true); c.fail { return false } @@ -175,19 +175,19 @@ func (i Interval) shouldRun(a *App) bool { if c := checkExceptionRanges(i.exceptionRanges); c.fail { return false } - if c := checkEnabledEntity(a.state, i.enabledEntities); c.fail { + if c := checkEnabledEntity(app.state, i.enabledEntities); c.fail { return false } - if c := checkDisabledEntity(a.state, i.disabledEntities); c.fail { + if c := checkDisabledEntity(app.state, i.disabledEntities); c.fail { return false } return true } -func (i *Interval) run(a *App) { - i.callback(a.service, a.state) +func (i *Interval) run(app *App) { + i.callback(app.service, app.state) } -func (i *Interval) updateNextRunTime(a *App) { +func (i *Interval) updateNextRunTime(app *App) { i.nextRunTime = i.nextRunTime.Add(i.frequency) } diff --git a/schedule.go b/schedule.go index 7b2aeeb..4019844 100644 --- a/schedule.go +++ b/schedule.go @@ -150,10 +150,10 @@ func (sb scheduleBuilderEnd) Build() *DailySchedule { return sb.schedule } -func (s *DailySchedule) initializeNextRunTime(a *App) { +func (s *DailySchedule) initializeNextRunTime(app *App) { // realStartTime already set for sunset/sunrise if s.isSunrise || s.isSunset { - s.nextRunTime = getNextSunRiseOrSet(a, s.isSunrise, s.sunOffset).Carbon2Time() + s.nextRunTime = getNextSunRiseOrSet(app, s.isSunrise, s.sunOffset).Carbon2Time() return } @@ -172,34 +172,34 @@ func (s *DailySchedule) getNextRunTime() time.Time { return s.nextRunTime } -func (s *DailySchedule) shouldRun(a *App) bool { +func (s *DailySchedule) shouldRun(app *App) bool { if c := checkExceptionDates(s.exceptionDates); c.fail { return false } if c := checkAllowlistDates(s.allowlistDates); c.fail { return false } - if c := checkEnabledEntity(a.state, s.enabledEntities); c.fail { + if c := checkEnabledEntity(app.state, s.enabledEntities); c.fail { return false } - if c := checkDisabledEntity(a.state, s.disabledEntities); c.fail { + if c := checkDisabledEntity(app.state, s.disabledEntities); c.fail { return false } return true } -func (s *DailySchedule) run(a *App) { - s.callback(a.service, a.state) +func (s *DailySchedule) run(app *App) { + s.callback(app.service, app.state) } -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 ec5eecead6f880950b64c3bf6e7976036ca94b06 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 00:32:33 +0200 Subject: [PATCH 034/103] Split up overlong lines --- app.go | 28 +++++++++++++++----- checkers.go | 3 ++- entitylistener.go | 32 ++++++++++++++-------- eventListener.go | 47 +++++++++++++++++++++++---------- example/example_live_test.go | 22 ++++++++++----- internal/internal.go | 10 +++++-- internal/websocket/websocket.go | 5 +++- interval.go | 40 +++++++++++++++++++--------- schedule.go | 36 ++++++++++++++++--------- state.go | 6 ++++- 10 files changed, 162 insertions(+), 67 deletions(-) diff --git a/app.go b/app.go index a6c016b..5cbbcea 100644 --- a/app.go +++ b/app.go @@ -93,7 +93,10 @@ type NewAppConfig struct { func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { if config.RESTBaseURI == "" || config.WebsocketURI == "" || config.HAAuthToken == "" || config.HomeZoneEntityId == "" { - slog.Error("RESTBaseURI, WebsocketURI, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") + slog.Error( + "RESTBaseURI, WebsocketURI, HAAuthToken, and HomeZoneEntityId " + + "are all required arguments in NewAppRequest", + ) return nil, ErrInvalidArgs } @@ -157,7 +160,10 @@ type NewAppRequest struct { // `App.Close()` must eventually be called to release resources. func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { if request.IpAddress == "" || request.HAAuthToken == "" || request.HomeZoneEntityId == "" { - slog.Error("IpAddress, HAAuthToken, and HomeZoneEntityId are all required arguments in NewAppRequest") + slog.Error( + "IpAddress, HAAuthToken, and HomeZoneEntityId " + + "are all required arguments in NewAppRequest", + ) return nil, ErrInvalidArgs } port := request.Port @@ -250,9 +256,13 @@ func (app *App) RegisterEventListeners(evls ...EventListener) { } } -func getSunriseSunset(s *StateImpl, sunrise bool, dateToUse carbon.Carbon, offset ...DurationString) carbon.Carbon { +func getSunriseSunset( + s *StateImpl, sunrise bool, dateToUse carbon.Carbon, offset ...DurationString, +) carbon.Carbon { date := dateToUse.Carbon2Time() - rise, set := sunriseLib.SunriseSunset(s.latitude, s.longitude, date.Year(), date.Month(), date.Day()) + rise, set := sunriseLib.SunriseSunset( + s.latitude, s.longitude, date.Year(), date.Month(), date.Day(), + ) rise, set = rise.Local(), set.Local() val := set @@ -269,7 +279,10 @@ func getSunriseSunset(s *StateImpl, sunrise bool, dateToUse carbon.Carbon, offse if len(offset) == 1 { t, err = time.ParseDuration(string(offset[0])) if err != nil { - parsingErr := fmt.Errorf("could not parse offset passed to %s: \"%s\": %w", printString, offset[0], err) + parsingErr := fmt.Errorf( + "could not parse offset passed to %s: \"%s\": %w", + printString, offset[0], err, + ) slog.Error(parsingErr.Error()) panic(parsingErr) } @@ -331,7 +344,10 @@ func (app *App) Start(ctx context.Context) error { if etl.runOnStartup && !etl.runOnStartupCompleted { entityState, err := app.state.Get(eid) if err != nil { - slog.Warn("Failed to get entity state \"", eid, "\" during startup, skipping RunOnStartup") + slog.Warn( + "Failed to get entity state \"", eid, + "\" during startup, skipping RunOnStartup", + ) } etl.runOnStartupCompleted = true diff --git a/checkers.go b/checkers.go index 1936190..029f10e 100644 --- a/checkers.go +++ b/checkers.go @@ -20,7 +20,8 @@ func checkWithinTimeRange(startTime, endTime string) conditionCheck { parsedEnd := internal.ParseTime(endTime) // check for midnight overlap - if parsedEnd.Lt(parsedStart) { // example turn on night lights when motion from 23:00 to 07:00 + if parsedEnd.Lt(parsedStart) { + // example turn on night lights when motion from 23:00 to 07:00 if parsedEnd.IsPast() { // such as at 15:00, 22:00 parsedEnd = parsedEnd.AddDay() } else { diff --git a/entitylistener.go b/entitylistener.go index 9272327..b8acd09 100644 --- a/entitylistener.go +++ b/entitylistener.go @@ -144,7 +144,9 @@ func (b elBuilder3) ExceptionDates(t time.Time, tl ...time.Time) elBuilder3 { } func (b elBuilder3) ExceptionRange(start, end time.Time) elBuilder3 { - b.entityListener.exceptionRanges = append(b.entityListener.exceptionRanges, timeRange{start, end}) + b.entityListener.exceptionRanges = append( + b.entityListener.exceptionRanges, timeRange{start, end}, + ) return b } @@ -153,13 +155,17 @@ func (b elBuilder3) RunOnStartup() elBuilder3 { return b } -/* -Enable this listener only when the current state of {entityId} matches {state}. -If there is a network error while retrieving state, the listener runs if {runOnNetworkError} is true. -*/ +// Enable this listener only when the current state of {entityId} +// matches {state}. If there is a network error while retrieving +// state, the listener runs if {runOnNetworkError} is true. func (b elBuilder3) EnabledWhen(entityId, state string, runOnNetworkError bool) elBuilder3 { if entityId == "" { - panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state)) + panic( + fmt.Sprintf( + "entityId is empty in EnabledWhen entityId='%s' state='%s'", + entityId, state, + ), + ) } i := internal.EnabledDisabledInfo{ Entity: entityId, @@ -170,13 +176,17 @@ func (b elBuilder3) EnabledWhen(entityId, state string, runOnNetworkError bool) return b } -/* -Disable this listener when the current state of {entityId} matches {state}. -If there is a network error while retrieving state, the listener runs if {runOnNetworkError} is true. -*/ +// Disable this listener when the current state of {entityId} matches +// {state}. If there is a network error while retrieving state, the +// listener runs if {runOnNetworkError} is true. func (b elBuilder3) DisabledWhen(entityId, state string, runOnNetworkError bool) elBuilder3 { if entityId == "" { - panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state)) + panic( + fmt.Sprintf( + "entityId is empty in EnabledWhen entityId='%s' state='%s'", + entityId, state, + ), + ) } i := internal.EnabledDisabledInfo{ Entity: entityId, diff --git a/eventListener.go b/eventListener.go index b74e323..152190d 100644 --- a/eventListener.go +++ b/eventListener.go @@ -85,23 +85,35 @@ func (b eventListenerBuilder3) Throttle(s DurationString) eventListenerBuilder3 return b } -func (b eventListenerBuilder3) ExceptionDates(t time.Time, tl ...time.Time) eventListenerBuilder3 { +func (b eventListenerBuilder3) ExceptionDates( + t time.Time, tl ...time.Time, +) eventListenerBuilder3 { b.eventListener.exceptionDates = append(tl, t) return b } func (b eventListenerBuilder3) ExceptionRange(start, end time.Time) eventListenerBuilder3 { - b.eventListener.exceptionRanges = append(b.eventListener.exceptionRanges, timeRange{start, end}) + b.eventListener.exceptionRanges = append( + b.eventListener.exceptionRanges, + timeRange{start, end}, + ) return b } -/* -Enable this listener only when the current state of {entityId} matches {state}. -If there is a network error while retrieving state, the listener runs if {runOnNetworkError} is true. -*/ -func (b eventListenerBuilder3) EnabledWhen(entityId, state string, runOnNetworkError bool) eventListenerBuilder3 { +// Enable this listener only when the current state of {entityId} +// matches {state}. If there is a network error while retrieving +// state, the listener runs if {runOnNetworkError} is true. +func (b eventListenerBuilder3) EnabledWhen( + entityId, state string, runOnNetworkError bool, +) eventListenerBuilder3 { if entityId == "" { - panic(fmt.Sprintf("entityId is empty in eventListener EnabledWhen entityId='%s' state='%s' runOnNetworkError='%t'", entityId, state, runOnNetworkError)) + panic( + fmt.Sprintf( + "entityId is empty in eventListener EnabledWhen "+ + "entityId='%s' state='%s' runOnNetworkError='%t'", + entityId, state, runOnNetworkError, + ), + ) } i := internal.EnabledDisabledInfo{ Entity: entityId, @@ -112,13 +124,20 @@ func (b eventListenerBuilder3) EnabledWhen(entityId, state string, runOnNetworkE return b } -/* -Disable this listener when the current state of {entityId} matches {state}. -If there is a network error while retrieving state, the listener runs if {runOnNetworkError} is true. -*/ -func (b eventListenerBuilder3) DisabledWhen(entityId, state string, runOnNetworkError bool) eventListenerBuilder3 { +// Disable this listener when the current state of {entityId} matches +// {state}. If there is a network error while retrieving state, the +// listener runs if {runOnNetworkError} is true. +func (b eventListenerBuilder3) DisabledWhen( + entityId, state string, runOnNetworkError bool, +) eventListenerBuilder3 { if entityId == "" { - panic(fmt.Sprintf("entityId is empty in eventListener EnabledWhen entityId='%s' state='%s' runOnNetworkError='%t'", entityId, state, runOnNetworkError)) + panic( + fmt.Sprintf( + "entityId is empty in eventListener EnabledWhen "+ + "entityId='%s' state='%s' runOnNetworkError='%t'", + entityId, state, runOnNetworkError, + ), + ) } i := internal.EnabledDisabledInfo{ Entity: entityId, diff --git a/example/example_live_test.go b/example/example_live_test.go index 37bc07c..8ee1c3c 100644 --- a/example/example_live_test.go +++ b/example/example_live_test.go @@ -107,11 +107,16 @@ func (s *MySuite) TestLightService() { initState := getEntityState(s, entityId) s.app.GetService().Light.Toggle(entityId) - assert.EventuallyWithT(s.T(), func(c *assert.CollectT) { - newState := getEntityState(s, entityId) - assert.NotEqual(c, initState, newState) - assert.True(c, s.suiteCtx["entityCallbackInvoked"].(bool)) - }, 10*time.Second, 1*time.Second, "State of light entity did not change or callback was not invoked") + assert.EventuallyWithT( + s.T(), + func(c *assert.CollectT) { + newState := getEntityState(s, entityId) + assert.NotEqual(c, initState, newState) + assert.True(c, s.suiteCtx["entityCallbackInvoked"].(bool)) + }, + 10*time.Second, 1*time.Second, + "State of light entity did not change or callback was not invoked", + ) } else { s.T().Skip("No light entity id provided") } @@ -126,7 +131,12 @@ func (s *MySuite) TestSchedule() { // Capture event after light entity state has changed func (s *MySuite) entityCallback(se *ga.Service, st ga.State, e ga.EntityData) { - slog.Info("Entity callback called.", "entity id", e.TriggerEntityId, "from state", e.FromState, "to state", e.ToState) + slog.Info( + "Entity callback called.", + "entity id", e.TriggerEntityId, + "from state", e.FromState, + "to state", e.ToState, + ) s.suiteCtx["entityCallbackInvoked"] = true } diff --git a/internal/internal.go b/internal/internal.go index f5efc14..1f566fc 100644 --- a/internal/internal.go +++ b/internal/internal.go @@ -27,7 +27,9 @@ func GetId() int64 { func ParseTime(s string) carbon.Carbon { t, err := time.Parse("15:04", s) if err != nil { - parsingErr := fmt.Errorf("failed to parse time string \"%s\"; format must be HH:MM.: %w", s, err) + parsingErr := fmt.Errorf( + "failed to parse time string \"%s\"; format must be HH:MM.: %w", s, err, + ) slog.Error(parsingErr.Error()) panic(parsingErr) } @@ -37,7 +39,11 @@ func ParseTime(s string) carbon.Carbon { func ParseDuration(s string) time.Duration { d, err := time.ParseDuration(s) if err != nil { - parsingErr := fmt.Errorf("couldn't parse string duration: \"%s\" see https://pkg.go.dev/time#ParseDuration for valid time units: %w", s, err) + parsingErr := fmt.Errorf( + "couldn't parse string duration: \"%s\" see "+ + "https://pkg.go.dev/time#ParseDuration for valid time units: %w", + s, err, + ) slog.Error(parsingErr.Error()) panic(parsingErr) } diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 0ee2bdc..c956193 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -88,7 +88,10 @@ func NewConnFromURI(ctx context.Context, uri string, authToken string) (*Conn, e // Verify auth message was successful err = conn.verifyAuthResponse() if err != nil { - slog.Error("Auth token is invalid. Please double check it or create a new token in your Home Assistant profile\n") + slog.Error( + "Auth token is invalid. Please double check it " + + "or create a new token in your Home Assistant profile\n", + ) return nil, err } diff --git a/interval.go b/interval.go index c983cad..0ba977f 100644 --- a/interval.go +++ b/interval.go @@ -25,7 +25,9 @@ type Interval struct { } func (i *Interval) Hash() string { - return fmt.Sprint(i.startTime, i.endTime, i.frequency, i.callback, i.exceptionDates, i.exceptionRanges) + return fmt.Sprint( + i.startTime, i.endTime, i.frequency, i.callback, i.exceptionDates, i.exceptionRanges, + ) } // Call @@ -107,13 +109,19 @@ func (ib intervalBuilderEnd) ExceptionRange(start, end time.Time) intervalBuilde return ib } -/* -Enable this interval only when the current state of {entityId} matches {state}. -If there is a network error while retrieving state, the interval runs if {runOnNetworkError} is true. -*/ -func (ib intervalBuilderEnd) EnabledWhen(entityId, state string, runOnNetworkError bool) intervalBuilderEnd { +// Enable this interval only when the current state of {entityId} +// matches {state}. If there is a network error while retrieving +// state, the interval runs if {runOnNetworkError} is true. +func (ib intervalBuilderEnd) EnabledWhen( + entityId, state string, runOnNetworkError bool, +) intervalBuilderEnd { if entityId == "" { - panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state)) + panic( + fmt.Sprintf( + "entityId is empty in EnabledWhen entityId='%s' state='%s'", + entityId, state, + ), + ) } i := internal.EnabledDisabledInfo{ Entity: entityId, @@ -124,13 +132,19 @@ func (ib intervalBuilderEnd) EnabledWhen(entityId, state string, runOnNetworkErr return ib } -/* -Disable this interval when the current state of {entityId} matches {state}. -If there is a network error while retrieving state, the interval runs if {runOnNetworkError} is true. -*/ -func (ib intervalBuilderEnd) DisabledWhen(entityId, state string, runOnNetworkError bool) intervalBuilderEnd { +// Disable this interval when the current state of {entityId} matches +// {state}. If there is a network error while retrieving state, the +// interval runs if {runOnNetworkError} is true. +func (ib intervalBuilderEnd) DisabledWhen( + entityId, state string, runOnNetworkError bool, +) intervalBuilderEnd { if entityId == "" { - panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state)) + panic( + fmt.Sprintf( + "entityId is empty in EnabledWhen entityId='%s' state='%s'", + entityId, state, + ), + ) } i := internal.EnabledDisabledInfo{ Entity: entityId, diff --git a/schedule.go b/schedule.go index 4019844..9590144 100644 --- a/schedule.go +++ b/schedule.go @@ -112,13 +112,19 @@ func (sb scheduleBuilderEnd) OnlyOnDates(t time.Time, tl ...time.Time) scheduleB return sb } -/* -Enable this schedule only when the current state of {entityId} matches {state}. -If there is a network error while retrieving state, the schedule runs if {runOnNetworkError} is true. -*/ -func (sb scheduleBuilderEnd) EnabledWhen(entityId, state string, runOnNetworkError bool) scheduleBuilderEnd { +// Enable this schedule only when the current state of {entityId} +// matches {state}. If there is a network error while retrieving +// state, the schedule runs if {runOnNetworkError} is true. +func (sb scheduleBuilderEnd) EnabledWhen( + entityId, state string, runOnNetworkError bool, +) scheduleBuilderEnd { if entityId == "" { - panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state)) + panic( + fmt.Sprintf( + "entityId is empty in EnabledWhen entityId='%s' state='%s'", + entityId, state, + ), + ) } i := internal.EnabledDisabledInfo{ Entity: entityId, @@ -129,13 +135,19 @@ func (sb scheduleBuilderEnd) EnabledWhen(entityId, state string, runOnNetworkErr return sb } -/* -Disable this schedule when the current state of {entityId} matches {state}. -If there is a network error while retrieving state, the schedule runs if {runOnNetworkError} is true. -*/ -func (sb scheduleBuilderEnd) DisabledWhen(entityId, state string, runOnNetworkError bool) scheduleBuilderEnd { +// Disable this schedule when the current state of {entityId} matches +// {state}. If there is a network error while retrieving state, the +// schedule runs if {runOnNetworkError} is true. +func (sb scheduleBuilderEnd) DisabledWhen( + entityId, state string, runOnNetworkError bool, +) scheduleBuilderEnd { if entityId == "" { - panic(fmt.Sprintf("entityId is empty in EnabledWhen entityId='%s' state='%s'", entityId, state)) + panic( + fmt.Sprintf( + "entityId is empty in EnabledWhen entityId='%s' state='%s'", + entityId, state, + ), + ) } i := internal.EnabledDisabledInfo{ Entity: entityId, diff --git a/state.go b/state.go index edc9c91..369422d 100644 --- a/state.go +++ b/state.go @@ -45,7 +45,11 @@ func newState(c *http.HttpClient, homeZoneEntityId string) (*StateImpl, error) { func (s *StateImpl) getLatLong(c *http.HttpClient, homeZoneEntityId string) error { resp, err := s.Get(homeZoneEntityId) if err != nil { - return fmt.Errorf("couldn't get latitude/longitude from home assistant entity '%s'. Did you type it correctly? It should be a zone like 'zone.home'", homeZoneEntityId) + return fmt.Errorf( + "couldn't get latitude/longitude from home assistant entity '%s'. "+ + "Did you type it correctly? It should be a zone like 'zone.home'", + homeZoneEntityId, + ) } if resp.Attributes["latitude"] != nil { From c7e894fd384ecfc5d2c3f9dd8d6d6beb548ce40c Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 14:08:44 +0200 Subject: [PATCH 035/103] Always get the next message ID from `Conn` --- internal/internal.go | 7 ---- internal/services/alarm_control_panel.go | 14 ++++---- internal/services/climate.go | 4 +-- internal/services/cover.go | 20 +++++------ internal/services/event.go | 3 +- internal/services/homeassistant.go | 6 ++-- internal/services/input_boolean.go | 8 ++--- internal/services/input_button.go | 4 +-- internal/services/input_datetime.go | 4 +-- internal/services/input_number.go | 8 ++--- internal/services/input_text.go | 4 +-- internal/services/light.go | 6 ++-- internal/services/lock.go | 4 +-- internal/services/media_player.go | 44 ++++++++++++------------ internal/services/notify.go | 2 +- internal/services/number.go | 2 +- internal/services/scene.go | 8 ++--- internal/services/script.go | 8 ++--- internal/services/services.go | 5 ++- internal/services/switch.go | 6 ++-- internal/services/tts.go | 6 ++-- internal/services/vacuum.go | 22 ++++++------ internal/services/zwavejs.go | 2 +- internal/websocket/reader.go | 14 ++++++++ internal/websocket/websocket.go | 30 ---------------- 25 files changed, 108 insertions(+), 133 deletions(-) diff --git a/internal/internal.go b/internal/internal.go index 1f566fc..c730a0a 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/alarm_control_panel.go b/internal/services/alarm_control_panel.go index d7a6c90..456706e 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -16,7 +16,7 @@ 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_away" if len(serviceData) != 0 { @@ -30,7 +30,7 @@ func (acp AlarmControlPanel) ArmAway(entityId string, serviceData ...map[string] // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_custom_bypass" if len(serviceData) != 0 { @@ -44,7 +44,7 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData .. // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmHome(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_home" if len(serviceData) != 0 { @@ -58,7 +58,7 @@ func (acp AlarmControlPanel) ArmHome(entityId string, serviceData ...map[string] // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmNight(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_night" if len(serviceData) != 0 { @@ -72,7 +72,7 @@ func (acp AlarmControlPanel) ArmNight(entityId string, serviceData ...map[string // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_vacation" if len(serviceData) != 0 { @@ -86,7 +86,7 @@ func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData ...map[str // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) Disarm(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_disarm" if len(serviceData) != 0 { @@ -100,7 +100,7 @@ func (acp AlarmControlPanel) Disarm(entityId string, serviceData ...map[string]a // Takes an entityId and an optional // map that is translated into service_data. func (acp AlarmControlPanel) Trigger(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_trigger" if len(serviceData) != 0 { diff --git a/internal/services/climate.go b/internal/services/climate.go index 7a6e861..c4bf569 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -14,7 +14,7 @@ type Climate struct { /* Public API */ func (c Climate) SetFanMode(entityId string, fanMode string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "climate" req.Service = "set_fan_mode" req.ServiceData = map[string]any{"fan_mode": fanMode} @@ -23,7 +23,7 @@ func (c Climate) SetFanMode(entityId string, fanMode string) { } func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatureRequest) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "climate" req.Service = "set_temperature" req.ServiceData = serviceData.ToJSON() diff --git a/internal/services/cover.go b/internal/services/cover.go index 537d31b..b6ada87 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -14,7 +14,7 @@ type Cover struct { // Close all or specified cover. Takes an entityId. func (c Cover) Close(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "close_cover" @@ -23,7 +23,7 @@ func (c Cover) Close(entityId string) { // Close all or specified cover tilt. Takes an entityId. func (c Cover) CloseTilt(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "close_cover_tilt" @@ -32,7 +32,7 @@ func (c Cover) CloseTilt(entityId string) { // Open all or specified cover. Takes an entityId. func (c Cover) Open(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "open_cover" @@ -41,7 +41,7 @@ func (c Cover) Open(entityId string) { // Open all or specified cover tilt. Takes an entityId. func (c Cover) OpenTilt(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "open_cover_tilt" @@ -51,7 +51,7 @@ func (c Cover) OpenTilt(entityId string) { // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "set_cover_position" if len(serviceData) != 0 { @@ -64,7 +64,7 @@ func (c Cover) SetPosition(entityId string, serviceData ...map[string]any) { // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "set_cover_tilt_position" if len(serviceData) != 0 { @@ -76,7 +76,7 @@ func (c Cover) SetTiltPosition(entityId string, serviceData ...map[string]any) { // Stop a cover entity. Takes an entityId. func (c Cover) Stop(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "stop_cover" @@ -85,7 +85,7 @@ func (c Cover) Stop(entityId string) { // Stop a cover entity tilt. Takes an entityId. func (c Cover) StopTilt(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "stop_cover_tilt" @@ -94,7 +94,7 @@ func (c Cover) StopTilt(entityId string) { // Toggle a cover open/closed. Takes an entityId. func (c Cover) Toggle(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "toggle" @@ -103,7 +103,7 @@ func (c Cover) Toggle(entityId string) { // Toggle a cover tilt open/closed. Takes an entityId. func (c Cover) ToggleTilt(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "toggle_cover_tilt" diff --git a/internal/services/event.go b/internal/services/event.go index acec7c2..4da7d8f 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -1,7 +1,6 @@ package services import ( - "saml.dev/gome-assistant/internal" "saml.dev/gome-assistant/internal/websocket" ) @@ -23,7 +22,7 @@ type FireEventRequest struct { // as `event_data`. func (e Event) Fire(eventType string, eventData ...map[string]any) { req := FireEventRequest{ - Id: internal.GetId(), + Id: e.conn.NextID(), Type: "fire_event", } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index b743033..34e39db 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -11,7 +11,7 @@ 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ha.conn, entityId) req.Domain = "homeassistant" req.Service = "turn_on" if len(serviceData) != 0 { @@ -24,7 +24,7 @@ func (ha *HomeAssistant) TurnOn(entityId string, serviceData ...map[string]any) // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ha.conn, entityId) req.Domain = "homeassistant" req.Service = "toggle" if len(serviceData) != 0 { @@ -35,7 +35,7 @@ func (ha *HomeAssistant) Toggle(entityId string, serviceData ...map[string]any) } func (ha *HomeAssistant) TurnOff(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ha.conn, entityId) req.Domain = "homeassistant" req.Service = "turn_off" diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 345662e..ff8172c 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -13,7 +13,7 @@ type InputBoolean struct { /* Public API */ func (ib InputBoolean) TurnOn(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_boolean" req.Service = "turn_on" @@ -21,7 +21,7 @@ func (ib InputBoolean) TurnOn(entityId string) { } func (ib InputBoolean) Toggle(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_boolean" req.Service = "toggle" @@ -29,14 +29,14 @@ func (ib InputBoolean) Toggle(entityId string) { } func (ib InputBoolean) TurnOff(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_boolean" req.Service = "turn_off" ib.conn.WriteMessage(req) } func (ib InputBoolean) Reload() { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_boolean" req.Service = "reload" ib.conn.WriteMessage(req) diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 8e881c1..29681c8 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -13,7 +13,7 @@ type InputButton struct { /* Public API */ func (ib InputButton) Press(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_button" req.Service = "press" @@ -21,7 +21,7 @@ func (ib InputButton) Press(entityId string) { } func (ib InputButton) Reload() { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_button" req.Service = "reload" ib.conn.WriteMessage(req) diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index ffd6344..ea35dd4 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -16,7 +16,7 @@ type InputDatetime struct { /* Public API */ func (ib InputDatetime) Set(entityId string, value time.Time) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_datetime" req.Service = "set_datetime" req.ServiceData = map[string]any{ @@ -27,7 +27,7 @@ func (ib InputDatetime) Set(entityId string, value time.Time) { } func (ib InputDatetime) Reload() { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_datetime" req.Service = "reload" ib.conn.WriteMessage(req) diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 55197f3..750618f 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -13,7 +13,7 @@ type InputNumber struct { /* Public API */ func (ib InputNumber) Set(entityId string, value float32) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_number" req.Service = "set_value" req.ServiceData = map[string]any{"value": value} @@ -22,7 +22,7 @@ func (ib InputNumber) Set(entityId string, value float32) { } func (ib InputNumber) Increment(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_number" req.Service = "increment" @@ -30,7 +30,7 @@ func (ib InputNumber) Increment(entityId string) { } func (ib InputNumber) Decrement(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_number" req.Service = "decrement" @@ -38,7 +38,7 @@ func (ib InputNumber) Decrement(entityId string) { } func (ib InputNumber) Reload() { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_number" req.Service = "reload" ib.conn.WriteMessage(req) diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 5f83f92..5ada028 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -13,7 +13,7 @@ type InputText struct { /* Public API */ func (ib InputText) Set(entityId string, value string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_text" req.Service = "set_value" req.ServiceData = map[string]any{ @@ -24,7 +24,7 @@ func (ib InputText) Set(entityId string, value string) { } func (ib InputText) Reload() { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_text" req.Service = "reload" ib.conn.WriteMessage(req) diff --git a/internal/services/light.go b/internal/services/light.go index 313f97c..eddfe11 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -15,7 +15,7 @@ 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "light" req.Service = "turn_on" if len(serviceData) != 0 { @@ -28,7 +28,7 @@ func (l Light) TurnOn(entityId string, serviceData ...map[string]any) { // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "light" req.Service = "toggle" if len(serviceData) != 0 { @@ -39,7 +39,7 @@ func (l Light) Toggle(entityId string, serviceData ...map[string]any) { } func (l Light) TurnOff(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "light" req.Service = "turn_off" l.conn.WriteMessage(req) diff --git a/internal/services/lock.go b/internal/services/lock.go index 9e8d780..b535734 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -15,7 +15,7 @@ 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "lock" req.Service = "lock" if len(serviceData) != 0 { @@ -28,7 +28,7 @@ func (l Lock) Lock(entityId string, serviceData ...map[string]any) { // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "lock" req.Service = "unlock" if len(serviceData) != 0 { diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 74ad896..a17319b 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -15,7 +15,7 @@ type MediaPlayer struct { // Send the media player the command to clear players playlist. // Takes an entityId. func (mp MediaPlayer) ClearPlaylist(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "clear_playlist" @@ -26,7 +26,7 @@ func (mp MediaPlayer) ClearPlaylist(entityId string) { // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) Join(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "join" if len(serviceData) != 0 { @@ -39,7 +39,7 @@ func (mp MediaPlayer) Join(entityId string, serviceData ...map[string]any) { // Send the media player the command for next track. // Takes an entityId. func (mp MediaPlayer) Next(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "media_next_track" @@ -49,7 +49,7 @@ func (mp MediaPlayer) Next(entityId string) { // Send the media player the command for pause. // Takes an entityId. func (mp MediaPlayer) Pause(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "media_pause" @@ -59,7 +59,7 @@ func (mp MediaPlayer) Pause(entityId string) { // Send the media player the command for play. // Takes an entityId. func (mp MediaPlayer) Play(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "media_play" @@ -69,7 +69,7 @@ func (mp MediaPlayer) Play(entityId string) { // Toggle media player play/pause state. // Takes an entityId. func (mp MediaPlayer) PlayPause(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "media_play_pause" @@ -79,7 +79,7 @@ func (mp MediaPlayer) PlayPause(entityId string) { // Send the media player the command for previous track. // Takes an entityId. func (mp MediaPlayer) Previous(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "media_previous_track" @@ -90,7 +90,7 @@ func (mp MediaPlayer) Previous(entityId string) { // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) Seek(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "media_seek" if len(serviceData) != 0 { @@ -103,7 +103,7 @@ func (mp MediaPlayer) Seek(entityId string, serviceData ...map[string]any) { // Send the media player the stop command. // Takes an entityId. func (mp MediaPlayer) Stop(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "media_stop" @@ -114,7 +114,7 @@ func (mp MediaPlayer) Stop(entityId string) { // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) PlayMedia(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "play_media" if len(serviceData) != 0 { @@ -127,7 +127,7 @@ func (mp MediaPlayer) PlayMedia(entityId string, serviceData ...map[string]any) // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "repeat_set" if len(serviceData) != 0 { @@ -141,7 +141,7 @@ func (mp MediaPlayer) RepeatSet(entityId string, serviceData ...map[string]any) // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "select_sound_mode" if len(serviceData) != 0 { @@ -155,7 +155,7 @@ func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData ...map[string // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) SelectSource(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "select_source" if len(serviceData) != 0 { @@ -169,7 +169,7 @@ func (mp MediaPlayer) SelectSource(entityId string, serviceData ...map[string]an // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) Shuffle(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "shuffle_set" if len(serviceData) != 0 { @@ -182,7 +182,7 @@ func (mp MediaPlayer) Shuffle(entityId string, serviceData ...map[string]any) { // Toggles a media player power state. // Takes an entityId. func (mp MediaPlayer) Toggle(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "toggle" @@ -192,7 +192,7 @@ func (mp MediaPlayer) Toggle(entityId string) { // Turn a media player power off. // Takes an entityId. func (mp MediaPlayer) TurnOff(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "turn_off" @@ -202,7 +202,7 @@ func (mp MediaPlayer) TurnOff(entityId string) { // Turn a media player power on. // Takes an entityId. func (mp MediaPlayer) TurnOn(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "turn_on" @@ -213,7 +213,7 @@ func (mp MediaPlayer) TurnOn(entityId string) { // platforms with support for player groups. // Takes an entityId. func (mp MediaPlayer) Unjoin(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "unjoin" @@ -223,7 +223,7 @@ func (mp MediaPlayer) Unjoin(entityId string) { // Turn a media player volume down. // Takes an entityId. func (mp MediaPlayer) VolumeDown(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "volume_down" @@ -234,7 +234,7 @@ func (mp MediaPlayer) VolumeDown(entityId string) { // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) VolumeMute(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "volume_mute" if len(serviceData) != 0 { @@ -248,7 +248,7 @@ func (mp MediaPlayer) VolumeMute(entityId string, serviceData ...map[string]any) // Takes an entityId and an optional // map that is translated into service_data. func (mp MediaPlayer) VolumeSet(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "volume_set" if len(serviceData) != 0 { @@ -261,7 +261,7 @@ func (mp MediaPlayer) VolumeSet(entityId string, serviceData ...map[string]any) // Turn a media player volume up. // Takes an entityId. func (mp MediaPlayer) VolumeUp(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "volume_up" diff --git a/internal/services/notify.go b/internal/services/notify.go index ab134cd..1972012 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -11,7 +11,7 @@ type Notify struct { // Send a notification. Takes a types.NotifyRequest. func (ha *Notify) Notify(reqData types.NotifyRequest) { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(ha.conn, "") req.Domain = "notify" req.Service = reqData.ServiceName diff --git a/internal/services/number.go b/internal/services/number.go index cc3916b..7ff8084 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -13,7 +13,7 @@ type Number struct { /* Public API */ func (ib Number) SetValue(entityId string, value float32) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "number" req.Service = "set_value" req.ServiceData = map[string]any{"value": value} diff --git a/internal/services/scene.go b/internal/services/scene.go index 2d2b34a..75682be 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -14,7 +14,7 @@ type Scene struct { // Apply a scene. Takes map that is translated into service_data. func (s Scene) Apply(serviceData ...map[string]any) { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(s.conn, "") req.Domain = "scene" req.Service = "apply" if len(serviceData) != 0 { @@ -27,7 +27,7 @@ func (s Scene) Apply(serviceData ...map[string]any) { // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "scene" req.Service = "create" if len(serviceData) != 0 { @@ -39,7 +39,7 @@ func (s Scene) Create(entityId string, serviceData ...map[string]any) { // Reload the scenes. func (s Scene) Reload() { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(s.conn, "") req.Domain = "scene" req.Service = "reload" @@ -49,7 +49,7 @@ func (s Scene) Reload() { // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "scene" req.Service = "turn_on" if len(serviceData) != 0 { diff --git a/internal/services/script.go b/internal/services/script.go index f203679..ea09e25 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -14,7 +14,7 @@ type Script struct { // Reload a script that was created in the HA UI. func (s Script) Reload(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "script" req.Service = "reload" @@ -23,7 +23,7 @@ func (s Script) Reload(entityId string) { // Toggle a script that was created in the HA UI. func (s Script) Toggle(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "script" req.Service = "toggle" @@ -32,7 +32,7 @@ func (s Script) Toggle(entityId string) { // Turn off a script that was created in the HA UI. func (s Script) TurnOff() { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(s.conn, "") req.Domain = "script" req.Service = "turn_off" @@ -41,7 +41,7 @@ func (s Script) TurnOff() { // Turn on a script that was created in the HA UI. func (s Script) TurnOn(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "script" req.Service = "turn_on" diff --git a/internal/services/services.go b/internal/services/services.go index 7297ece..be10418 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -1,7 +1,6 @@ package services import ( - "saml.dev/gome-assistant/internal" "saml.dev/gome-assistant/internal/websocket" ) @@ -42,8 +41,8 @@ type BaseServiceRequest struct { } `json:"target,omitempty"` } -func NewBaseServiceRequest(entityId string) BaseServiceRequest { - id := internal.GetId() +func NewBaseServiceRequest(conn *websocket.Conn, entityId string) BaseServiceRequest { + id := conn.NextID() bsr := BaseServiceRequest{ Id: id, RequestType: "call_service", diff --git a/internal/services/switch.go b/internal/services/switch.go index 49de778..de92dac 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -13,7 +13,7 @@ type Switch struct { /* Public API */ func (s Switch) TurnOn(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "switch" req.Service = "turn_on" @@ -21,7 +21,7 @@ func (s Switch) TurnOn(entityId string) { } func (s Switch) Toggle(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "switch" req.Service = "toggle" @@ -29,7 +29,7 @@ func (s Switch) Toggle(entityId string) { } func (s Switch) TurnOff(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "switch" req.Service = "turn_off" s.conn.WriteMessage(req) diff --git a/internal/services/tts.go b/internal/services/tts.go index 6f765bd..4265a38 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -14,7 +14,7 @@ type TTS struct { // Remove all text-to-speech cache files and RAM cache. func (tts TTS) ClearCache() { - req := NewBaseServiceRequest("") + req := NewBaseServiceRequest(tts.conn, "") req.Domain = "tts" req.Service = "clear_cache" @@ -25,7 +25,7 @@ func (tts TTS) ClearCache() { // Takes an entityId and an optional // map that is translated into service_data. func (tts TTS) CloudSay(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(tts.conn, entityId) req.Domain = "tts" req.Service = "cloud_say" if len(serviceData) != 0 { @@ -39,7 +39,7 @@ func (tts TTS) CloudSay(entityId string, serviceData ...map[string]any) { // Takes an entityId and an optional // map that is translated into service_data. func (tts TTS) GoogleTranslateSay(entityId string, serviceData ...map[string]any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(tts.conn, entityId) req.Domain = "tts" req.Service = "google_translate_say" if len(serviceData) != 0 { diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index e34ba73..dd5e946 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -15,7 +15,7 @@ type Vacuum struct { // Tell the vacuum cleaner to do a spot clean-up. // Takes an entityId. func (v Vacuum) CleanSpot(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "clean_spot" @@ -25,7 +25,7 @@ func (v Vacuum) CleanSpot(entityId string) { // Locate the vacuum cleaner robot. // Takes an entityId. func (v Vacuum) Locate(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "locate" @@ -35,7 +35,7 @@ func (v Vacuum) Locate(entityId string) { // Pause the cleaning task. // Takes an entityId. func (v Vacuum) Pause(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "pause" @@ -45,7 +45,7 @@ func (v Vacuum) Pause(entityId string) { // Tell the vacuum cleaner to return to its dock. // Takes an entityId. func (v Vacuum) ReturnToBase(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "return_to_base" @@ -55,7 +55,7 @@ func (v Vacuum) ReturnToBase(entityId string) { // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "send_command" if len(serviceData) != 0 { @@ -68,7 +68,7 @@ func (v Vacuum) SendCommand(entityId string, serviceData ...map[string]any) { // 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) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "set_fan_speed" @@ -82,7 +82,7 @@ func (v Vacuum) SetFanSpeed(entityId string, serviceData ...map[string]any) { // Start or resume the cleaning task. // Takes an entityId. func (v Vacuum) Start(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "start" @@ -92,7 +92,7 @@ func (v Vacuum) Start(entityId string) { // Start, pause, or resume the cleaning task. // Takes an entityId. func (v Vacuum) StartPause(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "start_pause" @@ -102,7 +102,7 @@ func (v Vacuum) StartPause(entityId string) { // Stop the current cleaning task. // Takes an entityId. func (v Vacuum) Stop(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "stop" @@ -112,7 +112,7 @@ func (v Vacuum) Stop(entityId string) { // Stop the current cleaning task and return to home. // Takes an entityId. func (v Vacuum) TurnOff(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "turn_off" @@ -122,7 +122,7 @@ func (v Vacuum) TurnOff(entityId string) { // Start a new cleaning task. // Takes an entityId. func (v Vacuum) TurnOn(entityId string) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "turn_on" diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index d9aae49..e59f919 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -14,7 +14,7 @@ type ZWaveJS struct { // ZWaveJS bulk_set_partial_config_parameters service. func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, value any) { - req := NewBaseServiceRequest(entityId) + req := NewBaseServiceRequest(zw.conn, entityId) req.Domain = "zwave_js" req.Service = "bulk_set_partial_config_parameters" req.ServiceData = map[string]any{ diff --git a/internal/websocket/reader.go b/internal/websocket/reader.go index d30345a..5a8f24e 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/reader.go @@ -19,6 +19,14 @@ type ChanMsg struct { Raw []byte } +func (conn *Conn) NextID() int64 { + conn.subscribeMutex.Lock() + defer conn.subscribeMutex.Unlock() + + conn.lastID++ + return conn.lastID +} + // subscribe creates a new (unique) subscription number and subscribes // `subscriber` to it. func (conn *Conn) subscribe(subscriber Subscriber) Subscription { @@ -55,6 +63,12 @@ func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { return subscriber, ok } +type SubEvent struct { + Id int64 `json:"id"` + Type string `json:"type"` + EventType string `json:"event_type"` +} + // WatchEvents subscribes to events of the given type, invoking // `subscriber` when any such events are received. Calls to // `subscriber` are synchronous with respect to any other received diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index c956193..6e9f90f 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -13,8 +13,6 @@ import ( "sync" "github.com/gorilla/websocket" - - "saml.dev/gome-assistant/internal" ) var ErrInvalidToken = errors.New("invalid authentication token") @@ -160,31 +158,3 @@ func (conn *Conn) verifyAuthResponse() error { return nil } - -type SubEvent struct { - Id int64 `json:"id"` - Type string `json:"type"` - EventType string `json:"event_type"` -} - -func (conn *Conn) SubscribeToEventType(eventType string, 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) - if err != nil { - wrappedErr := fmt.Errorf("error writing to websocket: %w", err) - slog.Error(wrappedErr.Error()) - panic(wrappedErr) - } - // m, _ := ReadMessage(conn, ctx) - // log.Default().Println(string(m)) -} From db5cf9067ae00c316e73dca9b3cf49bdf784b59d Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 18:29:40 +0200 Subject: [PATCH 036/103] Get rid of the wacky generic `BuildService()` function --- internal/services/alarm_control_panel.go | 6 ++++ internal/services/climate.go | 6 ++++ internal/services/cover.go | 6 ++++ internal/services/event.go | 6 ++++ internal/services/homeassistant.go | 6 ++++ internal/services/input_boolean.go | 6 ++++ internal/services/input_button.go | 6 ++++ internal/services/input_datetime.go | 6 ++++ internal/services/input_number.go | 6 ++++ internal/services/input_text.go | 6 ++++ internal/services/light.go | 6 ++++ internal/services/lock.go | 6 ++++ internal/services/media_player.go | 6 ++++ internal/services/notify.go | 6 ++++ internal/services/number.go | 6 ++++ internal/services/scene.go | 6 ++++ internal/services/script.go | 6 ++++ internal/services/services.go | 26 --------------- internal/services/switch.go | 6 ++++ internal/services/tts.go | 6 ++++ internal/services/vacuum.go | 6 ++++ internal/services/zwavejs.go | 6 ++++ service.go | 42 ++++++++++++------------ 23 files changed, 147 insertions(+), 47 deletions(-) diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 456706e..4f9fe82 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -12,6 +12,12 @@ type AlarmControlPanel struct { /* Public API */ +func NewAlarmControlPanel(conn *websocket.Conn) *AlarmControlPanel { + return &AlarmControlPanel{ + conn: conn, + } +} + // Send the alarm the command for arm away. // Takes an entityId and an optional // map that is translated into service_data. diff --git a/internal/services/climate.go b/internal/services/climate.go index c4bf569..5038277 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -11,6 +11,12 @@ type Climate struct { conn *websocket.Conn } +func NewClimate(conn *websocket.Conn) *Climate { + return &Climate{ + conn: conn, + } +} + /* Public API */ func (c Climate) SetFanMode(entityId string, fanMode string) { diff --git a/internal/services/cover.go b/internal/services/cover.go index b6ada87..435344f 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -10,6 +10,12 @@ type Cover struct { conn *websocket.Conn } +func NewCover(conn *websocket.Conn) *Cover { + return &Cover{ + conn: conn, + } +} + /* Public API */ // Close all or specified cover. Takes an entityId. diff --git a/internal/services/event.go b/internal/services/event.go index 4da7d8f..3f91194 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -8,6 +8,12 @@ type Event struct { conn *websocket.Conn } +func NewEvent(conn *websocket.Conn) *Event { + return &Event{ + conn: conn, + } +} + // Fire an event type FireEventRequest struct { Id int64 `json:"id"` diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 34e39db..d6e2306 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -8,6 +8,12 @@ type HomeAssistant struct { conn *websocket.Conn } +func NewHomeAssistant(conn *websocket.Conn) *HomeAssistant { + return &HomeAssistant{ + conn: conn, + } +} + // 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) { diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index ff8172c..d3b8778 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -10,6 +10,12 @@ type InputBoolean struct { conn *websocket.Conn } +func NewInputBoolean(conn *websocket.Conn) *InputBoolean { + return &InputBoolean{ + conn: conn, + } +} + /* Public API */ func (ib InputBoolean) TurnOn(entityId string) { diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 29681c8..1a94096 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -10,6 +10,12 @@ type InputButton struct { conn *websocket.Conn } +func NewInputButton(conn *websocket.Conn) *InputButton { + return &InputButton{ + conn: conn, + } +} + /* Public API */ func (ib InputButton) Press(entityId string) { diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index ea35dd4..d8d7d5e 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -13,6 +13,12 @@ type InputDatetime struct { conn *websocket.Conn } +func NewInputDatetime(conn *websocket.Conn) *InputDatetime { + return &InputDatetime{ + conn: conn, + } +} + /* Public API */ func (ib InputDatetime) Set(entityId string, value time.Time) { diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 750618f..d81dbf7 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -10,6 +10,12 @@ type InputNumber struct { conn *websocket.Conn } +func NewInputNumber(conn *websocket.Conn) *InputNumber { + return &InputNumber{ + conn: conn, + } +} + /* Public API */ func (ib InputNumber) Set(entityId string, value float32) { diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 5ada028..51bb5b3 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -10,6 +10,12 @@ type InputText struct { conn *websocket.Conn } +func NewInputText(conn *websocket.Conn) *InputText { + return &InputText{ + conn: conn, + } +} + /* Public API */ func (ib InputText) Set(entityId string, value string) { diff --git a/internal/services/light.go b/internal/services/light.go index eddfe11..1dd370c 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -10,6 +10,12 @@ type Light struct { conn *websocket.Conn } +func NewLight(conn *websocket.Conn) *Light { + return &Light{ + conn: conn, + } +} + /* Public API */ // TurnOn a light entity. Takes an entityId and an optional diff --git a/internal/services/lock.go b/internal/services/lock.go index b535734..f3b1841 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -10,6 +10,12 @@ type Lock struct { conn *websocket.Conn } +func NewLock(conn *websocket.Conn) *Lock { + return &Lock{ + conn: conn, + } +} + /* Public API */ // Lock a lock entity. Takes an entityId and an optional diff --git a/internal/services/media_player.go b/internal/services/media_player.go index a17319b..2909471 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -10,6 +10,12 @@ type MediaPlayer struct { conn *websocket.Conn } +func NewMediaPlayer(conn *websocket.Conn) *MediaPlayer { + return &MediaPlayer{ + conn: conn, + } +} + /* Public API */ // Send the media player the command to clear players playlist. diff --git a/internal/services/notify.go b/internal/services/notify.go index 1972012..0914dd7 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -9,6 +9,12 @@ type Notify struct { conn *websocket.Conn } +func NewNotify(conn *websocket.Conn) *Notify { + return &Notify{ + conn: conn, + } +} + // Send a notification. Takes a types.NotifyRequest. func (ha *Notify) Notify(reqData types.NotifyRequest) { req := NewBaseServiceRequest(ha.conn, "") diff --git a/internal/services/number.go b/internal/services/number.go index 7ff8084..e8883f9 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -10,6 +10,12 @@ type Number struct { conn *websocket.Conn } +func NewNumber(conn *websocket.Conn) *Number { + return &Number{ + conn: conn, + } +} + /* Public API */ func (ib Number) SetValue(entityId string, value float32) { diff --git a/internal/services/scene.go b/internal/services/scene.go index 75682be..1cc6c98 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -10,6 +10,12 @@ type Scene struct { conn *websocket.Conn } +func NewScene(conn *websocket.Conn) *Scene { + return &Scene{ + conn: conn, + } +} + /* Public API */ // Apply a scene. Takes map that is translated into service_data. diff --git a/internal/services/script.go b/internal/services/script.go index ea09e25..765d465 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -10,6 +10,12 @@ type Script struct { conn *websocket.Conn } +func NewScript(conn *websocket.Conn) *Script { + return &Script{ + conn: conn, + } +} + /* Public API */ // Reload a script that was created in the HA UI. diff --git a/internal/services/services.go b/internal/services/services.go index be10418..8f7f967 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -4,32 +4,6 @@ import ( "saml.dev/gome-assistant/internal/websocket" ) -func BuildService[ - T AlarmControlPanel | - Climate | - Cover | - Light | - HomeAssistant | - Lock | - MediaPlayer | - Switch | - InputBoolean | - InputButton | - InputDatetime | - InputText | - InputNumber | - Event | - Notify | - Number | - Scene | - Script | - TTS | - Vacuum | - ZWaveJS, -](conn *websocket.Conn) *T { - return &T{conn: conn} -} - type BaseServiceRequest struct { Id int64 `json:"id"` RequestType string `json:"type"` // hardcoded "call_service" diff --git a/internal/services/switch.go b/internal/services/switch.go index de92dac..6703b73 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -10,6 +10,12 @@ type Switch struct { conn *websocket.Conn } +func NewSwitch(conn *websocket.Conn) *Switch { + return &Switch{ + conn: conn, + } +} + /* Public API */ func (s Switch) TurnOn(entityId string) { diff --git a/internal/services/tts.go b/internal/services/tts.go index 4265a38..3560272 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -10,6 +10,12 @@ type TTS struct { conn *websocket.Conn } +func NewTTS(conn *websocket.Conn) *TTS { + return &TTS{ + conn: conn, + } +} + /* Public API */ // Remove all text-to-speech cache files and RAM cache. diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index dd5e946..2917fce 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -10,6 +10,12 @@ type Vacuum struct { conn *websocket.Conn } +func NewVacuum(conn *websocket.Conn) *Vacuum { + return &Vacuum{ + conn: conn, + } +} + /* Public API */ // Tell the vacuum cleaner to do a spot clean-up. diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index e59f919..ef6ea43 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -10,6 +10,12 @@ type ZWaveJS struct { conn *websocket.Conn } +func NewZWaveJS(conn *websocket.Conn) *ZWaveJS { + return &ZWaveJS{ + conn: conn, + } +} + /* Public API */ // ZWaveJS bulk_set_partial_config_parameters service. diff --git a/service.go b/service.go index 541ec5b..78cbd61 100644 --- a/service.go +++ b/service.go @@ -32,26 +32,26 @@ type Service struct { func newService(conn *websocket.Conn, httpClient *http.HttpClient) *Service { return &Service{ - 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), - TTS: services.BuildService[services.TTS](conn), - Vacuum: services.BuildService[services.Vacuum](conn), - ZWaveJS: services.BuildService[services.ZWaveJS](conn), + AlarmControlPanel: services.NewAlarmControlPanel(conn), + Climate: services.NewClimate(conn), + Cover: services.NewCover(conn), + Light: services.NewLight(conn), + HomeAssistant: services.NewHomeAssistant(conn), + Lock: services.NewLock(conn), + MediaPlayer: services.NewMediaPlayer(conn), + Switch: services.NewSwitch(conn), + InputBoolean: services.NewInputBoolean(conn), + InputButton: services.NewInputButton(conn), + InputText: services.NewInputText(conn), + InputDatetime: services.NewInputDatetime(conn), + InputNumber: services.NewInputNumber(conn), + Event: services.NewEvent(conn), + Notify: services.NewNotify(conn), + Number: services.NewNumber(conn), + Scene: services.NewScene(conn), + Script: services.NewScript(conn), + TTS: services.NewTTS(conn), + Vacuum: services.NewVacuum(conn), + ZWaveJS: services.NewZWaveJS(conn), } } From c7810760487fa3815bfcd0d0e2d1739f23251f18 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 20:32:55 +0200 Subject: [PATCH 037/103] Change how message IDs are allocated and messages are sent 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. --- internal/services/alarm_control_panel.go | 35 +++++-- internal/services/climate.go | 10 +- internal/services/cover.go | 50 ++++++++-- internal/services/event.go | 6 +- internal/services/homeassistant.go | 15 ++- internal/services/input_boolean.go | 22 ++++- internal/services/input_button.go | 11 ++- internal/services/input_datetime.go | 11 ++- internal/services/input_number.go | 21 ++++- internal/services/input_text.go | 11 ++- internal/services/light.go | 16 +++- internal/services/lock.go | 10 +- internal/services/media_player.go | 110 +++++++++++++++++----- internal/services/notify.go | 6 +- internal/services/number.go | 5 +- internal/services/scene.go | 20 +++- internal/services/script.go | 20 +++- internal/services/services.go | 2 - internal/services/switch.go | 16 +++- internal/services/tts.go | 15 ++- internal/services/vacuum.go | 55 ++++++++--- internal/services/zwavejs.go | 5 +- internal/websocket/{reader.go => read.go} | 73 +++++++------- internal/websocket/send.go | 71 ++++++++++++++ internal/websocket/websocket.go | 26 ++--- 25 files changed, 499 insertions(+), 143 deletions(-) rename internal/websocket/{reader.go => read.go} (70%) create mode 100644 internal/websocket/send.go diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 4f9fe82..09829f7 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -29,7 +29,10 @@ func (acp AlarmControlPanel) ArmAway(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req) + acp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the alarm the command for arm away. @@ -43,7 +46,10 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData .. req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req) + acp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the alarm the command for arm home. @@ -57,7 +63,10 @@ func (acp AlarmControlPanel) ArmHome(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req) + acp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the alarm the command for arm night. @@ -71,7 +80,10 @@ func (acp AlarmControlPanel) ArmNight(entityId string, serviceData ...map[string req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req) + acp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the alarm the command for arm vacation. @@ -85,7 +97,10 @@ func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData ...map[str req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req) + acp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the alarm the command for disarm. @@ -99,7 +114,10 @@ func (acp AlarmControlPanel) Disarm(entityId string, serviceData ...map[string]a req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req) + acp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the alarm the command for trigger. @@ -113,5 +131,8 @@ func (acp AlarmControlPanel) Trigger(entityId string, serviceData ...map[string] req.ServiceData = serviceData[0] } - acp.conn.WriteMessage(req) + acp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/climate.go b/internal/services/climate.go index 5038277..b6b8d78 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -25,7 +25,10 @@ func (c Climate) SetFanMode(entityId string, fanMode string) { req.Service = "set_fan_mode" req.ServiceData = map[string]any{"fan_mode": fanMode} - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatureRequest) { @@ -34,5 +37,8 @@ func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatur req.Service = "set_temperature" req.ServiceData = serviceData.ToJSON() - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/cover.go b/internal/services/cover.go index 435344f..941253f 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -24,7 +24,10 @@ func (c Cover) Close(entityId string) { req.Domain = "cover" req.Service = "close_cover" - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Close all or specified cover tilt. Takes an entityId. @@ -33,7 +36,10 @@ func (c Cover) CloseTilt(entityId string) { req.Domain = "cover" req.Service = "close_cover_tilt" - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Open all or specified cover. Takes an entityId. @@ -42,7 +48,10 @@ func (c Cover) Open(entityId string) { req.Domain = "cover" req.Service = "open_cover" - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Open all or specified cover tilt. Takes an entityId. @@ -51,7 +60,10 @@ func (c Cover) OpenTilt(entityId string) { req.Domain = "cover" req.Service = "open_cover_tilt" - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Move to specific position all or specified cover. Takes an entityId and an optional @@ -64,7 +76,10 @@ func (c Cover) SetPosition(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Move to specific position all or specified cover tilt. Takes an entityId and an optional @@ -77,7 +92,10 @@ func (c Cover) SetTiltPosition(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Stop a cover entity. Takes an entityId. @@ -86,7 +104,10 @@ func (c Cover) Stop(entityId string) { req.Domain = "cover" req.Service = "stop_cover" - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Stop a cover entity tilt. Takes an entityId. @@ -95,7 +116,10 @@ func (c Cover) StopTilt(entityId string) { req.Domain = "cover" req.Service = "stop_cover_tilt" - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Toggle a cover open/closed. Takes an entityId. @@ -104,7 +128,10 @@ func (c Cover) Toggle(entityId string) { req.Domain = "cover" req.Service = "toggle" - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Toggle a cover tilt open/closed. Takes an entityId. @@ -113,5 +140,8 @@ func (c Cover) ToggleTilt(entityId string) { req.Domain = "cover" req.Service = "toggle_cover_tilt" - c.conn.WriteMessage(req) + c.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/event.go b/internal/services/event.go index 3f91194..85490bd 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -28,7 +28,6 @@ type FireEventRequest struct { // as `event_data`. func (e Event) Fire(eventType string, eventData ...map[string]any) { req := FireEventRequest{ - Id: e.conn.NextID(), Type: "fire_event", } @@ -37,5 +36,8 @@ func (e Event) Fire(eventType string, eventData ...map[string]any) { req.EventData = eventData[0] } - e.conn.WriteMessage(req) + e.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index d6e2306..9f74d5c 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -24,7 +24,10 @@ func (ha *HomeAssistant) TurnOn(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - ha.conn.WriteMessage(req) + ha.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Toggle a Home Assistant entity. Takes an entityId and an optional @@ -37,7 +40,10 @@ func (ha *HomeAssistant) Toggle(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - ha.conn.WriteMessage(req) + ha.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ha *HomeAssistant) TurnOff(entityId string) { @@ -45,5 +51,8 @@ func (ha *HomeAssistant) TurnOff(entityId string) { req.Domain = "homeassistant" req.Service = "turn_off" - ha.conn.WriteMessage(req) + ha.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index d3b8778..95cf100 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -23,7 +23,10 @@ func (ib InputBoolean) TurnOn(entityId string) { req.Domain = "input_boolean" req.Service = "turn_on" - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputBoolean) Toggle(entityId string) { @@ -31,19 +34,30 @@ func (ib InputBoolean) Toggle(entityId string) { req.Domain = "input_boolean" req.Service = "toggle" - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputBoolean) TurnOff(entityId string) { req := NewBaseServiceRequest(ib.conn, entityId) req.Domain = "input_boolean" req.Service = "turn_off" - ib.conn.WriteMessage(req) + + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputBoolean) Reload() { req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_boolean" req.Service = "reload" - ib.conn.WriteMessage(req) + + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 1a94096..58ed020 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -23,12 +23,19 @@ func (ib InputButton) Press(entityId string) { req.Domain = "input_button" req.Service = "press" - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputButton) Reload() { req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_button" req.Service = "reload" - ib.conn.WriteMessage(req) + + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index d8d7d5e..e6de5db 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -29,12 +29,19 @@ func (ib InputDatetime) Set(entityId string, value time.Time) { "timestamp": fmt.Sprint(value.Unix()), } - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputDatetime) Reload() { req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_datetime" req.Service = "reload" - ib.conn.WriteMessage(req) + + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index d81dbf7..4bfafc2 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -24,7 +24,10 @@ func (ib InputNumber) Set(entityId string, value float32) { req.Service = "set_value" req.ServiceData = map[string]any{"value": value} - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputNumber) Increment(entityId string) { @@ -32,7 +35,10 @@ func (ib InputNumber) Increment(entityId string) { req.Domain = "input_number" req.Service = "increment" - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputNumber) Decrement(entityId string) { @@ -40,12 +46,19 @@ func (ib InputNumber) Decrement(entityId string) { req.Domain = "input_number" req.Service = "decrement" - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputNumber) Reload() { req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_number" req.Service = "reload" - ib.conn.WriteMessage(req) + + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 51bb5b3..546e764 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -26,12 +26,19 @@ func (ib InputText) Set(entityId string, value string) { "value": value, } - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (ib InputText) Reload() { req := NewBaseServiceRequest(ib.conn, "") req.Domain = "input_text" req.Service = "reload" - ib.conn.WriteMessage(req) + + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/light.go b/internal/services/light.go index 1dd370c..00f1a8a 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -28,7 +28,10 @@ func (l Light) TurnOn(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(req) + l.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Toggle a light entity. Takes an entityId and an optional @@ -41,12 +44,19 @@ func (l Light) Toggle(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(req) + l.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (l Light) TurnOff(entityId string) { req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "light" req.Service = "turn_off" - l.conn.WriteMessage(req) + + l.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/lock.go b/internal/services/lock.go index f3b1841..1d4a140 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -28,7 +28,10 @@ func (l Lock) Lock(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(req) + l.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Unlock a lock entity. Takes an entityId and an optional @@ -41,5 +44,8 @@ func (l Lock) Unlock(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - l.conn.WriteMessage(req) + l.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 2909471..a24d9cf 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -25,7 +25,10 @@ func (mp MediaPlayer) ClearPlaylist(entityId string) { req.Domain = "media_player" req.Service = "clear_playlist" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Group players together. Only works on platforms with support for player groups. @@ -39,7 +42,10 @@ func (mp MediaPlayer) Join(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the command for next track. @@ -49,7 +55,10 @@ func (mp MediaPlayer) Next(entityId string) { req.Domain = "media_player" req.Service = "media_next_track" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the command for pause. @@ -59,7 +68,10 @@ func (mp MediaPlayer) Pause(entityId string) { req.Domain = "media_player" req.Service = "media_pause" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the command for play. @@ -69,7 +81,10 @@ func (mp MediaPlayer) Play(entityId string) { req.Domain = "media_player" req.Service = "media_play" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Toggle media player play/pause state. @@ -79,7 +94,10 @@ func (mp MediaPlayer) PlayPause(entityId string) { req.Domain = "media_player" req.Service = "media_play_pause" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the command for previous track. @@ -89,7 +107,10 @@ func (mp MediaPlayer) Previous(entityId string) { req.Domain = "media_player" req.Service = "media_previous_track" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the command to seek in current playing media. @@ -103,7 +124,10 @@ func (mp MediaPlayer) Seek(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the stop command. @@ -113,7 +137,10 @@ func (mp MediaPlayer) Stop(entityId string) { req.Domain = "media_player" req.Service = "media_stop" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the command for playing media. @@ -127,7 +154,10 @@ func (mp MediaPlayer) PlayMedia(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Set repeat mode. Takes an entityId and an optional @@ -140,7 +170,10 @@ func (mp MediaPlayer) RepeatSet(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the command to change sound mode. @@ -154,7 +187,10 @@ func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData ...map[string req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send the media player the command to change input source. @@ -168,7 +204,10 @@ func (mp MediaPlayer) SelectSource(entityId string, serviceData ...map[string]an req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Set shuffling state. @@ -182,7 +221,10 @@ func (mp MediaPlayer) Shuffle(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Toggles a media player power state. @@ -192,7 +234,10 @@ func (mp MediaPlayer) Toggle(entityId string) { req.Domain = "media_player" req.Service = "toggle" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Turn a media player power off. @@ -202,7 +247,10 @@ func (mp MediaPlayer) TurnOff(entityId string) { req.Domain = "media_player" req.Service = "turn_off" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Turn a media player power on. @@ -212,7 +260,10 @@ func (mp MediaPlayer) TurnOn(entityId string) { req.Domain = "media_player" req.Service = "turn_on" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Unjoin the player from a group. Only works on @@ -223,7 +274,10 @@ func (mp MediaPlayer) Unjoin(entityId string) { req.Domain = "media_player" req.Service = "unjoin" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Turn a media player volume down. @@ -233,7 +287,10 @@ func (mp MediaPlayer) VolumeDown(entityId string) { req.Domain = "media_player" req.Service = "volume_down" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Mute a media player's volume. @@ -247,7 +304,10 @@ func (mp MediaPlayer) VolumeMute(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Set a media player's volume level. @@ -261,7 +321,10 @@ func (mp MediaPlayer) VolumeSet(entityId string, serviceData ...map[string]any) req.ServiceData = serviceData[0] } - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Turn a media player volume up. @@ -271,5 +334,8 @@ func (mp MediaPlayer) VolumeUp(entityId string) { req.Domain = "media_player" req.Service = "volume_up" - mp.conn.WriteMessage(req) + mp.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/notify.go b/internal/services/notify.go index 0914dd7..b1320b4 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -29,5 +29,9 @@ func (ha *Notify) Notify(reqData types.NotifyRequest) { } req.ServiceData = serviceData - ha.conn.WriteMessage(req) + + ha.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/number.go b/internal/services/number.go index e8883f9..c94e69f 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -24,5 +24,8 @@ func (ib Number) SetValue(entityId string, value float32) { req.Service = "set_value" req.ServiceData = map[string]any{"value": value} - ib.conn.WriteMessage(req) + ib.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/scene.go b/internal/services/scene.go index 1cc6c98..19766e7 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -27,7 +27,10 @@ func (s Scene) Apply(serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Create a scene entity. Takes an entityId and an optional @@ -40,7 +43,10 @@ func (s Scene) Create(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Reload the scenes. @@ -49,7 +55,10 @@ func (s Scene) Reload() { req.Domain = "scene" req.Service = "reload" - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // TurnOn a scene entity. Takes an entityId and an optional @@ -62,5 +71,8 @@ func (s Scene) TurnOn(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/script.go b/internal/services/script.go index 765d465..5d431df 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -24,7 +24,10 @@ func (s Script) Reload(entityId string) { req.Domain = "script" req.Service = "reload" - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Toggle a script that was created in the HA UI. @@ -33,7 +36,10 @@ func (s Script) Toggle(entityId string) { req.Domain = "script" req.Service = "toggle" - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Turn off a script that was created in the HA UI. @@ -42,7 +48,10 @@ func (s Script) TurnOff() { req.Domain = "script" req.Service = "turn_off" - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Turn on a script that was created in the HA UI. @@ -51,5 +60,8 @@ func (s Script) TurnOn(entityId string) { req.Domain = "script" req.Service = "turn_on" - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/services.go b/internal/services/services.go index 8f7f967..2b6dde8 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -16,9 +16,7 @@ type BaseServiceRequest struct { } func NewBaseServiceRequest(conn *websocket.Conn, entityId string) BaseServiceRequest { - id := conn.NextID() bsr := BaseServiceRequest{ - Id: id, RequestType: "call_service", } if entityId != "" { diff --git a/internal/services/switch.go b/internal/services/switch.go index 6703b73..d9aaed6 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -23,7 +23,10 @@ func (s Switch) TurnOn(entityId string) { req.Domain = "switch" req.Service = "turn_on" - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (s Switch) Toggle(entityId string) { @@ -31,12 +34,19 @@ func (s Switch) Toggle(entityId string) { req.Domain = "switch" req.Service = "toggle" - s.conn.WriteMessage(req) + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } func (s Switch) TurnOff(entityId string) { req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "switch" req.Service = "turn_off" - s.conn.WriteMessage(req) + + s.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/tts.go b/internal/services/tts.go index 3560272..a2bde18 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -24,7 +24,10 @@ func (tts TTS) ClearCache() { req.Domain = "tts" req.Service = "clear_cache" - tts.conn.WriteMessage(req) + tts.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Say something using text-to-speech on a media player with cloud. @@ -38,7 +41,10 @@ func (tts TTS) CloudSay(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - tts.conn.WriteMessage(req) + tts.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Say something using text-to-speech on a media player with google_translate. @@ -52,5 +58,8 @@ func (tts TTS) GoogleTranslateSay(entityId string, serviceData ...map[string]any req.ServiceData = serviceData[0] } - tts.conn.WriteMessage(req) + tts.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 2917fce..710ce3f 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -25,7 +25,10 @@ func (v Vacuum) CleanSpot(entityId string) { req.Domain = "vacuum" req.Service = "clean_spot" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Locate the vacuum cleaner robot. @@ -35,7 +38,10 @@ func (v Vacuum) Locate(entityId string) { req.Domain = "vacuum" req.Service = "locate" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Pause the cleaning task. @@ -45,7 +51,10 @@ func (v Vacuum) Pause(entityId string) { req.Domain = "vacuum" req.Service = "pause" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Tell the vacuum cleaner to return to its dock. @@ -55,7 +64,10 @@ func (v Vacuum) ReturnToBase(entityId string) { req.Domain = "vacuum" req.Service = "return_to_base" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Send a raw command to the vacuum cleaner. Takes an entityId and an optional @@ -68,7 +80,10 @@ func (v Vacuum) SendCommand(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Set the fan speed of the vacuum cleaner. Takes an entityId and an optional @@ -82,7 +97,10 @@ func (v Vacuum) SetFanSpeed(entityId string, serviceData ...map[string]any) { req.ServiceData = serviceData[0] } - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Start or resume the cleaning task. @@ -92,7 +110,10 @@ func (v Vacuum) Start(entityId string) { req.Domain = "vacuum" req.Service = "start" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Start, pause, or resume the cleaning task. @@ -102,7 +123,10 @@ func (v Vacuum) StartPause(entityId string) { req.Domain = "vacuum" req.Service = "start_pause" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Stop the current cleaning task. @@ -112,7 +136,10 @@ func (v Vacuum) Stop(entityId string) { req.Domain = "vacuum" req.Service = "stop" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Stop the current cleaning task and return to home. @@ -122,7 +149,10 @@ func (v Vacuum) TurnOff(entityId string) { req.Domain = "vacuum" req.Service = "turn_off" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } // Start a new cleaning task. @@ -132,5 +162,8 @@ func (v Vacuum) TurnOn(entityId string) { req.Domain = "vacuum" req.Service = "turn_on" - v.conn.WriteMessage(req) + v.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index ef6ea43..58d892b 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -28,5 +28,8 @@ func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, valu "value": value, } - zw.conn.WriteMessage(req) + zw.conn.Send(func(mw websocket.MessageWriter) error { + req.Id = mw.NextID() + return mw.SendMessage(req) + }) } diff --git a/internal/websocket/reader.go b/internal/websocket/read.go similarity index 70% rename from internal/websocket/reader.go rename to internal/websocket/read.go index 5a8f24e..a0dce06 100644 --- a/internal/websocket/reader.go +++ b/internal/websocket/read.go @@ -19,35 +19,10 @@ type ChanMsg struct { Raw []byte } -func (conn *Conn) NextID() int64 { - conn.subscribeMutex.Lock() - defer conn.subscribeMutex.Unlock() - - conn.lastID++ - return conn.lastID -} - -// subscribe creates a new (unique) subscription number and subscribes -// `subscriber` to it. -func (conn *Conn) subscribe(subscriber Subscriber) Subscription { - conn.subscribeMutex.Lock() - defer conn.subscribeMutex.Unlock() - - conn.lastID++ - id := conn.lastID - conn.subscribers[id] = subscriber - return Subscription{ - conn: conn, - id: conn.lastID, - } -} - // unsubscribe unsubscribes from `subscription`. It must be called -// exactly once for each subscription. +// exactly once for each subscription. It must be invoked while +// holding the `subscribeMutex` for writing. func (conn *Conn) unsubscribe(id int64) error { - conn.subscribeMutex.Lock() - defer conn.subscribeMutex.Unlock() - if _, ok := conn.subscribers[id]; !ok { return fmt.Errorf("subscription ID %d wasn't active", id) } @@ -75,16 +50,21 @@ type SubEvent struct { // messages, but asynchronous with respect to writes. func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscription, error) { // Make sure we're listening before events might start arriving: - subscription := conn.subscribe(subscriber) - e := SubEvent{ - Id: subscription.ID(), Type: "subscribe_events", EventType: eventType, } - err := conn.WriteMessage(e) + var subscription Subscription + err := conn.Send(func(mw MessageWriter) error { + subscription = mw.Subscribe(subscriber) + e.Id = subscription.ID() + if err := mw.SendMessage(e); err != nil { + conn.unsubscribe(subscription.ID()) + return fmt.Errorf("error writing to websocket: %w", err) + } + return nil + }) if err != nil { - conn.unsubscribe(subscription.ID()) return Subscription{}, fmt.Errorf("error writing to websocket: %w", err) } // m, _ := ReadMessage(conn, ctx) @@ -92,6 +72,35 @@ func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscrip return subscription, nil } +type UnsubEvent struct { + Id int64 `json:"id"` + Type string `json:"type"` + Subscription int64 `json:"subscription"` +} + +// unwatchEvents unsubscribes to events with the given `subscriptionID`. This does +// not remove the subscriber. +func (conn *Conn) unwatchEvents(subscriptionID int64) error { + conn.subscribeMutex.Lock() + defer conn.subscribeMutex.Unlock() + + e := UnsubEvent{ + Type: "unsubscribe_events", + Subscription: subscriptionID, + } + + err := conn.Send(func(mw MessageWriter) error { + e.Id = mw.NextID() + return mw.SendMessage(e) + }) + if err != nil { + return fmt.Errorf("unsubscribing from ID %d: %w", subscriptionID, err) + } + // m, _ := ReadMessage(conn, ctx) + // log.Default().Println(string(m)) + return nil +} + func (conn *Conn) WatchStateChangedEvents(subscriber Subscriber) (Subscription, error) { return conn.WatchEvents("state_changed", subscriber) } diff --git a/internal/websocket/send.go b/internal/websocket/send.go new file mode 100644 index 0000000..e582196 --- /dev/null +++ b/internal/websocket/send.go @@ -0,0 +1,71 @@ +package websocket + +import "fmt" + +type MessageWriter interface { + NextID() int64 + Subscribe(subscriber Subscriber) Subscription + SendMessage(msg any) error +} + +// Messager is called by `Send()` while holding the `writeMutex`. It +// can send a message by allocating an ID using `mw.NextID()` then +// sending it using `mw.SendMessage()`. The `MessageWriter` should +// only be used while the callback is running. +type Messager func(mw MessageWriter) 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 and passed the ID that it should use. +// +// Usage: +// +// msg := NewFooMessage{…} +// err := conn.Send(func(mw MessageWriter) error { +// id := mw.NextID() +// // …do anything else that needs to be done with `id`… +// msg.ID = id +// return mw.SendMessage(msg) +// }) +func (conn *Conn) Send(msgr Messager) error { + conn.writeMutex.Lock() + defer conn.writeMutex.Unlock() + + return msgr(connMessageWriter{conn: conn}) +} + +// 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. +func (mw connMessageWriter) SendMessage(msg any) error { + if err := mw.conn.conn.WriteJSON(msg); err != nil { + return fmt.Errorf("sending websocket message to server: %w", err) + } + + return nil +} + +type connMessageWriter struct { + conn *Conn +} + +func (mw connMessageWriter) NextID() int64 { + mw.conn.lastID++ + return mw.conn.lastID +} + +// Subscribe creates a new (unique) subscription ID and subscribes +// `subscriber` to it, in the sense that the subscriber will be called +// for any responses that have that ID. This doesn't actually interact +// with the server. +func (mw connMessageWriter) Subscribe(subscriber Subscriber) Subscription { + id := mw.NextID() + mw.conn.subscribers[id] = subscriber + return Subscription{ + conn: mw.conn, + id: id, + } +} diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 6e9f90f..cfb51b4 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -27,9 +27,11 @@ type Conn struct { conn *websocket.Conn subscribeMutex sync.RWMutex - // lastID is the last message ID that has already been used. - lastID int64 - subscribers map[int64]Subscriber + subscribers map[int64]Subscriber + + // lastID is the last message ID that has already been used. It + // must be accessed atomically. + lastID int64 } // Subscriber is called synchronously when a message with the @@ -50,9 +52,13 @@ func (subscription *Subscription) Cancel() { return } - // FIXME: this should also unsubscribe at the server. + subscription.conn.subscribeMutex.Lock() + defer subscription.conn.subscribeMutex.Unlock() subscription.conn.unsubscribe(subscription.id) + + subscription.conn.unwatchEvents(subscription.id) + subscription.id = 0 } @@ -106,18 +112,6 @@ func NewSecureConn(ctx context.Context, ip, port, authToken string) (*Conn, erro return NewConnFromURI(ctx, uri, authToken) } -func (conn *Conn) WriteMessage(msg interface{}) error { - conn.writeMutex.Lock() - defer conn.writeMutex.Unlock() - - err := conn.conn.WriteJSON(msg) - if err != nil { - return err - } - - return nil -} - func (conn *Conn) readMessage() ([]byte, error) { _, msg, err := conn.conn.ReadMessage() if err != nil { From b396ae773a30116e16703ef584af4408d8ad3400 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 20:56:54 +0200 Subject: [PATCH 038/103] Make `serviceData`, where supported, non-optional --- example/example.go | 4 +- example/example_live_test.go | 2 +- internal/services/alarm_control_panel.go | 42 ++++++------------ internal/services/cover.go | 12 ++---- internal/services/event.go | 6 +-- internal/services/homeassistant.go | 12 ++---- internal/services/light.go | 12 ++---- internal/services/lock.go | 12 ++---- internal/services/media_player.go | 54 ++++++++---------------- internal/services/scene.go | 18 +++----- internal/services/tts.go | 12 ++---- internal/services/vacuum.go | 13 ++---- internal/websocket/read.go | 3 -- 13 files changed, 67 insertions(+), 135 deletions(-) diff --git a/example/example.go b/example/example.go index 60fc713..96479e2 100644 --- a/example/example.go +++ b/example/example.go @@ -61,7 +61,7 @@ func main() { func pantryLights(service *ga.Service, state ga.State, sensor ga.EntityData) { l := "light.pantry" if sensor.ToState == "on" { - service.HomeAssistant.TurnOn(l) + service.HomeAssistant.TurnOn(l, nil) } else { service.HomeAssistant.TurnOff(l) } @@ -94,6 +94,6 @@ func lightsOut(service *ga.Service, state ga.State) { } func sunriseSched(service *ga.Service, state ga.State) { - service.Light.TurnOn("light.living_room_lamps") + service.Light.TurnOn("light.living_room_lamps", nil) service.Light.TurnOff("light.christmas_lights") } diff --git a/example/example_live_test.go b/example/example_live_test.go index 8ee1c3c..268964f 100644 --- a/example/example_live_test.go +++ b/example/example_live_test.go @@ -105,7 +105,7 @@ func (s *MySuite) TestLightService() { if entityId != "" { initState := getEntityState(s, entityId) - s.app.GetService().Light.Toggle(entityId) + s.app.GetService().Light.Toggle(entityId, nil) assert.EventuallyWithT( s.T(), diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 09829f7..f0952ec 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -21,13 +21,11 @@ func NewAlarmControlPanel(conn *websocket.Conn) *AlarmControlPanel { // Send the alarm the command for arm away. // Takes an entityId and an optional // map that is translated into service_data. -func (acp AlarmControlPanel) ArmAway(entityId string, serviceData ...map[string]any) { +func (acp AlarmControlPanel) ArmAway(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_away" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -38,13 +36,11 @@ func (acp AlarmControlPanel) ArmAway(entityId string, serviceData ...map[string] // 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) { +func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_custom_bypass" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -55,13 +51,11 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData .. // 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) { +func (acp AlarmControlPanel) ArmHome(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_home" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -72,13 +66,11 @@ func (acp AlarmControlPanel) ArmHome(entityId string, serviceData ...map[string] // 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) { +func (acp AlarmControlPanel) ArmNight(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_night" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -89,13 +81,11 @@ func (acp AlarmControlPanel) ArmNight(entityId string, serviceData ...map[string // 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) { +func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_arm_vacation" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -106,13 +96,11 @@ func (acp AlarmControlPanel) ArmVacation(entityId string, serviceData ...map[str // 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) { +func (acp AlarmControlPanel) Disarm(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_disarm" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -123,13 +111,11 @@ func (acp AlarmControlPanel) Disarm(entityId string, serviceData ...map[string]a // 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) { +func (acp AlarmControlPanel) Trigger(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(acp.conn, entityId) req.Domain = "alarm_control_panel" req.Service = "alarm_trigger" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/cover.go b/internal/services/cover.go index 941253f..828ceef 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -68,13 +68,11 @@ func (c Cover) OpenTilt(entityId string) { // 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) { +func (c Cover) SetPosition(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "set_cover_position" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData c.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -84,13 +82,11 @@ func (c Cover) SetPosition(entityId string, serviceData ...map[string]any) { // 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) { +func (c Cover) SetTiltPosition(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(c.conn, entityId) req.Domain = "cover" req.Service = "set_cover_tilt_position" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData c.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/event.go b/internal/services/event.go index 85490bd..61827e9 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -26,15 +26,13 @@ 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) { +func (e Event) Fire(eventType string, eventData map[string]any) { req := FireEventRequest{ Type: "fire_event", } req.EventType = eventType - if len(eventData) != 0 { - req.EventData = eventData[0] - } + req.EventData = eventData e.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 9f74d5c..8f013f5 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -16,13 +16,11 @@ func NewHomeAssistant(conn *websocket.Conn) *HomeAssistant { // 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) { +func (ha *HomeAssistant) TurnOn(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(ha.conn, entityId) req.Domain = "homeassistant" req.Service = "turn_on" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData ha.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -32,13 +30,11 @@ func (ha *HomeAssistant) TurnOn(entityId string, serviceData ...map[string]any) // 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) { +func (ha *HomeAssistant) Toggle(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(ha.conn, entityId) req.Domain = "homeassistant" req.Service = "toggle" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData ha.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/light.go b/internal/services/light.go index 00f1a8a..e3ddae2 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -20,13 +20,11 @@ func NewLight(conn *websocket.Conn) *Light { // 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) { +func (l Light) TurnOn(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "light" req.Service = "turn_on" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData l.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -36,13 +34,11 @@ func (l Light) TurnOn(entityId string, serviceData ...map[string]any) { // 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) { +func (l Light) Toggle(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "light" req.Service = "toggle" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData l.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/lock.go b/internal/services/lock.go index 1d4a140..b902ecf 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -20,13 +20,11 @@ func NewLock(conn *websocket.Conn) *Lock { // 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) { +func (l Lock) Lock(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "lock" req.Service = "lock" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData l.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -36,13 +34,11 @@ func (l Lock) Lock(entityId string, serviceData ...map[string]any) { // 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) { +func (l Lock) Unlock(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(l.conn, entityId) req.Domain = "lock" req.Service = "unlock" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData l.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/media_player.go b/internal/services/media_player.go index a24d9cf..0d268f6 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -34,13 +34,11 @@ func (mp MediaPlayer) ClearPlaylist(entityId string) { // 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) { +func (mp MediaPlayer) Join(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "join" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -116,13 +114,11 @@ func (mp MediaPlayer) Previous(entityId string) { // 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) { +func (mp MediaPlayer) Seek(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "media_seek" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -146,13 +142,11 @@ func (mp MediaPlayer) Stop(entityId string) { // 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) { +func (mp MediaPlayer) PlayMedia(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "play_media" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -162,13 +156,11 @@ func (mp MediaPlayer) PlayMedia(entityId string, serviceData ...map[string]any) // 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) { +func (mp MediaPlayer) RepeatSet(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "repeat_set" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -179,13 +171,11 @@ func (mp MediaPlayer) RepeatSet(entityId string, serviceData ...map[string]any) // 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) { +func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "select_sound_mode" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -196,13 +186,11 @@ func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData ...map[string // 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) { +func (mp MediaPlayer) SelectSource(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "select_source" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -213,13 +201,11 @@ func (mp MediaPlayer) SelectSource(entityId string, serviceData ...map[string]an // 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) { +func (mp MediaPlayer) Shuffle(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "shuffle_set" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -296,13 +282,11 @@ func (mp MediaPlayer) VolumeDown(entityId string) { // 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) { +func (mp MediaPlayer) VolumeMute(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "volume_mute" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -313,13 +297,11 @@ func (mp MediaPlayer) VolumeMute(entityId string, serviceData ...map[string]any) // 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) { +func (mp MediaPlayer) VolumeSet(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(mp.conn, entityId) req.Domain = "media_player" req.Service = "volume_set" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/scene.go b/internal/services/scene.go index 19766e7..cb30dd3 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -19,13 +19,11 @@ func NewScene(conn *websocket.Conn) *Scene { /* Public API */ // Apply a scene. Takes map that is translated into service_data. -func (s Scene) Apply(serviceData ...map[string]any) { +func (s Scene) Apply(serviceData map[string]any) { req := NewBaseServiceRequest(s.conn, "") req.Domain = "scene" req.Service = "apply" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData s.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -35,13 +33,11 @@ func (s Scene) Apply(serviceData ...map[string]any) { // 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) { +func (s Scene) Create(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "scene" req.Service = "create" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData s.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -63,13 +59,11 @@ func (s Scene) Reload() { // 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) { +func (s Scene) TurnOn(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(s.conn, entityId) req.Domain = "scene" req.Service = "turn_on" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData s.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/tts.go b/internal/services/tts.go index a2bde18..bb322e8 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -33,13 +33,11 @@ func (tts TTS) ClearCache() { // 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) { +func (tts TTS) CloudSay(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(tts.conn, entityId) req.Domain = "tts" req.Service = "cloud_say" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData tts.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -50,13 +48,11 @@ func (tts TTS) CloudSay(entityId string, serviceData ...map[string]any) { // 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) { +func (tts TTS) GoogleTranslateSay(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(tts.conn, entityId) req.Domain = "tts" req.Service = "google_translate_say" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData tts.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 710ce3f..906bd78 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -72,13 +72,11 @@ func (v Vacuum) ReturnToBase(entityId string) { // 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) { +func (v Vacuum) SendCommand(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "send_command" - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData v.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() @@ -88,14 +86,11 @@ func (v Vacuum) SendCommand(entityId string, serviceData ...map[string]any) { // 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) { +func (v Vacuum) SetFanSpeed(entityId string, serviceData map[string]any) { req := NewBaseServiceRequest(v.conn, entityId) req.Domain = "vacuum" req.Service = "set_fan_speed" - - if len(serviceData) != 0 { - req.ServiceData = serviceData[0] - } + req.ServiceData = serviceData v.conn.Send(func(mw websocket.MessageWriter) error { req.Id = mw.NextID() diff --git a/internal/websocket/read.go b/internal/websocket/read.go index a0dce06..04a907e 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -81,9 +81,6 @@ type UnsubEvent struct { // unwatchEvents unsubscribes to events with the given `subscriptionID`. This does // not remove the subscriber. func (conn *Conn) unwatchEvents(subscriptionID int64) error { - conn.subscribeMutex.Lock() - defer conn.subscribeMutex.Unlock() - e := UnsubEvent{ Type: "unsubscribe_events", Subscription: subscriptionID, From f5b6f7ab1d27a85f75899a78814d2fdb88f0f69f Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 14 Apr 2024 21:37:04 +0200 Subject: [PATCH 039/103] =?UTF-8?q?Use=20more=20conventional=20capitalizat?= =?UTF-8?q?ions=20(e.g.,=20`Id`=20=E2=86=92=20`ID`)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- app.go | 24 +-- entitylistener.go | 38 ++--- eventListener.go | 28 ++-- example/example.go | 4 +- example/example_live_test.go | 28 ++-- internal/http/http.go | 4 +- internal/services/alarm_control_panel.go | 63 ++++---- internal/services/climate.go | 14 +- internal/services/cover.go | 90 ++++++----- internal/services/event.go | 4 +- internal/services/homeassistant.go | 25 +-- internal/services/input_boolean.go | 25 +-- internal/services/input_button.go | 11 +- internal/services/input_datetime.go | 11 +- internal/services/input_number.go | 25 +-- internal/services/input_text.go | 11 +- internal/services/light.go | 27 ++-- internal/services/lock.go | 20 +-- internal/services/media_player.go | 198 +++++++++++++---------- internal/services/notify.go | 4 +- internal/services/number.go | 7 +- internal/services/scene.go | 28 ++-- internal/services/script.go | 25 +-- internal/services/services.go | 31 ++-- internal/services/switch.go | 21 +-- internal/services/tts.go | 25 ++- internal/services/vacuum.go | 92 +++++------ internal/services/zwavejs.go | 7 +- internal/websocket/read.go | 16 +- interval.go | 24 +-- schedule.go | 24 +-- state.go | 22 +-- 33 files changed, 514 insertions(+), 466 deletions(-) diff --git a/README.md b/README.md index 820459e..e709e12 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ func myFunc(se *ga.Service, st *ga.State) { Entity Listeners are used to respond to entities changing state. The simplest entity listener looks like: ```go -etl := ga.NewEntityListener().EntityIds("binary_sensor.front_door").Call(myFunc).Build() +etl := ga.NewEntityListener().EntityIDs("binary_sensor.front_door").Call(myFunc).Build() ``` Entity listeners have other functions to change the behavior. @@ -139,7 +139,7 @@ func myFunc(se *ga.Service, st *ga.State, e ga.EntityData) { Event Listeners are used to respond to entities changing state. The simplest event listener looks like: ```go -evl := ga.NewEntityListener().EntityIds("binary_sensor.front_door").Call(myFunc).Build() +evl := ga.NewEntityListener().EntityIDs("binary_sensor.front_door").Call(myFunc).Build() ``` Event listeners have other functions to change the behavior. diff --git a/app.go b/app.go index 5cbbcea..8f6334e 100644 --- a/app.go +++ b/app.go @@ -79,10 +79,10 @@ type NewAppConfig struct { HAAuthToken string // Required - // EntityId of the zone representing your home e.g. "zone.home". + // EntityID of the zone representing your home e.g. "zone.home". // Used to pull latitude/longitude from Home Assistant // to calculate sunset/sunrise times. - HomeZoneEntityId string + HomeZoneEntityID string } // NewAppFromConfig establishes the websocket connection and returns @@ -92,9 +92,9 @@ type NewAppConfig struct { // app. func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { if config.RESTBaseURI == "" || config.WebsocketURI == "" || - config.HAAuthToken == "" || config.HomeZoneEntityId == "" { + config.HAAuthToken == "" || config.HomeZoneEntityID == "" { slog.Error( - "RESTBaseURI, WebsocketURI, HAAuthToken, and HomeZoneEntityId " + + "RESTBaseURI, WebsocketURI, HAAuthToken, and HomeZoneEntityID " + "are all required arguments in NewAppRequest", ) return nil, ErrInvalidArgs @@ -108,7 +108,7 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { httpClient := http.ClientFromUri(config.RESTBaseURI, config.HAAuthToken) service := newService(wsWriter, httpClient) - state, err := newState(httpClient, config.HomeZoneEntityId) + state, err := newState(httpClient, config.HomeZoneEntityID) if err != nil { return nil, err } @@ -141,10 +141,10 @@ type NewAppRequest struct { HAAuthToken string // Required - // EntityId of the zone representing your home e.g. "zone.home". + // EntityID of the zone representing your home e.g. "zone.home". // Used to pull latitude/longitude from Home Assistant // to calculate sunset/sunrise times. - HomeZoneEntityId string + HomeZoneEntityID string // Optional // Whether to use secure connections for http and websockets. @@ -159,9 +159,9 @@ type NewAppRequest struct { // cancel the app. If this function returns successfully, then // `App.Close()` must eventually be called to release resources. func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { - if request.IpAddress == "" || request.HAAuthToken == "" || request.HomeZoneEntityId == "" { + if request.IpAddress == "" || request.HAAuthToken == "" || request.HomeZoneEntityID == "" { slog.Error( - "IpAddress, HAAuthToken, and HomeZoneEntityId " + + "IpAddress, HAAuthToken, and HomeZoneEntityID " + "are all required arguments in NewAppRequest", ) return nil, ErrInvalidArgs @@ -173,7 +173,7 @@ func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { config := NewAppConfig{ HAAuthToken: request.HAAuthToken, - HomeZoneEntityId: request.HomeZoneEntityId, + HomeZoneEntityID: request.HomeZoneEntityID, } if request.Secure { @@ -222,7 +222,7 @@ func (app *App) RegisterEntityListeners(etls ...EntityListener) { panic(ErrInvalidArgs) } - for _, entity := range etl.entityIds { + for _, entity := range etl.entityIDs { if elList, ok := app.entityListeners[entity]; ok { app.entityListeners[entity] = append(elList, &etl) } else { @@ -354,7 +354,7 @@ func (app *App) Start(ctx context.Context) error { etl := etl eg.Go(func() error { etl.callback(app.service, app.state, EntityData{ - TriggerEntityId: eid, + TriggerEntityID: eid, FromState: entityState.State, FromAttributes: entityState.Attributes, ToState: entityState.State, diff --git a/entitylistener.go b/entitylistener.go index b8acd09..7e1eb3e 100644 --- a/entitylistener.go +++ b/entitylistener.go @@ -11,7 +11,7 @@ import ( ) type EntityListener struct { - entityIds []string + entityIDs []string callback EntityListenerCallback fromState string toState string @@ -37,7 +37,7 @@ type EntityListener struct { type EntityListenerCallback func(*Service, State, EntityData) type EntityData struct { - TriggerEntityId string + TriggerEntityID string FromState string FromAttributes map[string]any ToState string @@ -78,11 +78,11 @@ type elBuilder1 struct { entityListener EntityListener } -func (b elBuilder1) EntityIds(entityIds ...string) elBuilder2 { - if len(entityIds) == 0 { - panic("must pass at least one entityId to EntityIds()") +func (b elBuilder1) EntityIDs(entityIDs ...string) elBuilder2 { + if len(entityIDs) == 0 { + panic("must pass at least one entityID to EntityIDs()") } else { - b.entityListener.entityIds = entityIds + b.entityListener.entityIDs = entityIDs } return elBuilder2(b) } @@ -155,20 +155,20 @@ func (b elBuilder3) RunOnStartup() elBuilder3 { return b } -// Enable this listener only when the current state of {entityId} +// Enable this listener only when the current state of {entityID} // matches {state}. If there is a network error while retrieving // state, the listener runs if {runOnNetworkError} is true. -func (b elBuilder3) EnabledWhen(entityId, state string, runOnNetworkError bool) elBuilder3 { - if entityId == "" { +func (b elBuilder3) EnabledWhen(entityID, state string, runOnNetworkError bool) elBuilder3 { + if entityID == "" { panic( fmt.Sprintf( - "entityId is empty in EnabledWhen entityId='%s' state='%s'", - entityId, state, + "entityID is empty in EnabledWhen entityID='%s' state='%s'", + entityID, state, ), ) } i := internal.EnabledDisabledInfo{ - Entity: entityId, + Entity: entityID, State: state, RunOnError: runOnNetworkError, } @@ -176,20 +176,20 @@ func (b elBuilder3) EnabledWhen(entityId, state string, runOnNetworkError bool) return b } -// Disable this listener when the current state of {entityId} matches +// Disable this listener when the current state of {entityID} matches // {state}. If there is a network error while retrieving state, the // listener runs if {runOnNetworkError} is true. -func (b elBuilder3) DisabledWhen(entityId, state string, runOnNetworkError bool) elBuilder3 { - if entityId == "" { +func (b elBuilder3) DisabledWhen(entityID, state string, runOnNetworkError bool) elBuilder3 { + if entityID == "" { panic( fmt.Sprintf( - "entityId is empty in EnabledWhen entityId='%s' state='%s'", - entityId, state, + "entityID is empty in EnabledWhen entityID='%s' state='%s'", + entityID, state, ), ) } i := internal.EnabledDisabledInfo{ - Entity: entityId, + Entity: entityID, State: state, RunOnError: runOnNetworkError, } @@ -253,7 +253,7 @@ func (app *App) callEntityListeners(chanMsg websocket.ChanMsg) { } entityData := EntityData{ - TriggerEntityId: eid, + TriggerEntityID: eid, FromState: data.OldState.State, FromAttributes: data.OldState.Attributes, ToState: data.NewState.State, diff --git a/eventListener.go b/eventListener.go index 152190d..4ced5ab 100644 --- a/eventListener.go +++ b/eventListener.go @@ -100,23 +100,23 @@ func (b eventListenerBuilder3) ExceptionRange(start, end time.Time) eventListene return b } -// Enable this listener only when the current state of {entityId} +// Enable this listener only when the current state of {entityID} // matches {state}. If there is a network error while retrieving // state, the listener runs if {runOnNetworkError} is true. func (b eventListenerBuilder3) EnabledWhen( - entityId, state string, runOnNetworkError bool, + entityID, state string, runOnNetworkError bool, ) eventListenerBuilder3 { - if entityId == "" { + if entityID == "" { panic( fmt.Sprintf( - "entityId is empty in eventListener EnabledWhen "+ - "entityId='%s' state='%s' runOnNetworkError='%t'", - entityId, state, runOnNetworkError, + "entityID is empty in eventListener EnabledWhen "+ + "entityID='%s' state='%s' runOnNetworkError='%t'", + entityID, state, runOnNetworkError, ), ) } i := internal.EnabledDisabledInfo{ - Entity: entityId, + Entity: entityID, State: state, RunOnError: runOnNetworkError, } @@ -124,23 +124,23 @@ func (b eventListenerBuilder3) EnabledWhen( return b } -// Disable this listener when the current state of {entityId} matches +// Disable this listener when the current state of {entityID} matches // {state}. If there is a network error while retrieving state, the // listener runs if {runOnNetworkError} is true. func (b eventListenerBuilder3) DisabledWhen( - entityId, state string, runOnNetworkError bool, + entityID, state string, runOnNetworkError bool, ) eventListenerBuilder3 { - if entityId == "" { + if entityID == "" { panic( fmt.Sprintf( - "entityId is empty in eventListener EnabledWhen "+ - "entityId='%s' state='%s' runOnNetworkError='%t'", - entityId, state, runOnNetworkError, + "entityID is empty in eventListener EnabledWhen "+ + "entityID='%s' state='%s' runOnNetworkError='%t'", + entityID, state, runOnNetworkError, ), ) } i := internal.EnabledDisabledInfo{ - Entity: entityId, + Entity: entityID, State: state, RunOnError: runOnNetworkError, } diff --git a/example/example.go b/example/example.go index 96479e2..3975cd4 100644 --- a/example/example.go +++ b/example/example.go @@ -17,7 +17,7 @@ func main() { ga.NewAppRequest{ IpAddress: "192.168.86.67", // Replace with your Home Assistant IP Address HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), - HomeZoneEntityId: "zone.home", + HomeZoneEntityID: "zone.home", }, ) if err != nil { @@ -29,7 +29,7 @@ func main() { pantryDoor := ga. NewEntityListener(). - EntityIds("binary_sensor.pantry_door"). + EntityIDs("binary_sensor.pantry_door"). Call(pantryLights). Build() diff --git a/example/example_live_test.go b/example/example_live_test.go index 268964f..40d914c 100644 --- a/example/example_live_test.go +++ b/example/example_live_test.go @@ -27,10 +27,10 @@ type ( HAAuthToken string `yaml:"token"` IpAddress string `yaml:"address"` Port string `yaml:"port"` - HomeZoneEntityId string `yaml:"zone"` + HomeZoneEntityID string `yaml:"zone"` } Entities struct { - LightEntityId string `yaml:"light_entity_id"` + LightEntityID string `yaml:"light_entity_id"` } } ) @@ -67,7 +67,7 @@ func (s *MySuite) SetupSuite(ctx context.Context) { ga.NewAppRequest{ HAAuthToken: s.config.Hass.HAAuthToken, IpAddress: s.config.Hass.IpAddress, - HomeZoneEntityId: s.config.Hass.HomeZoneEntityId, + HomeZoneEntityID: s.config.Hass.HomeZoneEntityID, }, ) if err != nil { @@ -76,10 +76,10 @@ func (s *MySuite) SetupSuite(ctx context.Context) { } // Register all automations - entityId := s.config.Entities.LightEntityId - if entityId != "" { + entityID := s.config.Entities.LightEntityID + if entityID != "" { s.suiteCtx["entityCallbackInvoked"] = false - etl := ga.NewEntityListener().EntityIds(entityId).Call(s.entityCallback).Build() + etl := ga.NewEntityListener().EntityIDs(entityID).Call(s.entityCallback).Build() s.app.RegisterEntityListeners(etl) } @@ -101,16 +101,16 @@ func (s *MySuite) TearDownSuite() { // Basic test of light toggle service and entity listener func (s *MySuite) TestLightService() { - entityId := s.config.Entities.LightEntityId + entityID := s.config.Entities.LightEntityID - if entityId != "" { - initState := getEntityState(s, entityId) - s.app.GetService().Light.Toggle(entityId, nil) + if entityID != "" { + initState := getEntityState(s, entityID) + s.app.GetService().Light.Toggle(entityID, nil) assert.EventuallyWithT( s.T(), func(c *assert.CollectT) { - newState := getEntityState(s, entityId) + newState := getEntityState(s, entityID) assert.NotEqual(c, initState, newState) assert.True(c, s.suiteCtx["entityCallbackInvoked"].(bool)) }, @@ -133,7 +133,7 @@ func (s *MySuite) TestSchedule() { func (s *MySuite) entityCallback(se *ga.Service, st ga.State, e ga.EntityData) { slog.Info( "Entity callback called.", - "entity id", e.TriggerEntityId, + "entity id", e.TriggerEntityID, "from state", e.FromState, "to state", e.ToState, ) @@ -146,8 +146,8 @@ func (s *MySuite) dailyScheduleCallback(se *ga.Service, st ga.State) { s.suiteCtx["dailyScheduleCallbackInvoked"] = true } -func getEntityState(s *MySuite, entityId string) string { - state, err := s.app.GetState().Get(entityId) +func getEntityState(s *MySuite, entityID string) string { + state, err := s.app.GetState().Get(entityID) if err != nil { slog.Error("Error getting entity state", err) s.T().FailNow() diff --git a/internal/http/http.go b/internal/http/http.go index 2f89643..f541e4c 100644 --- a/internal/http/http.go +++ b/internal/http/http.go @@ -36,8 +36,8 @@ func ClientFromUri(uri, token string) *HttpClient { } } -func (c *HttpClient) GetState(entityId string) ([]byte, error) { - resp, err := get(c.url+"/states/"+entityId, c.token) +func (c *HttpClient) GetState(entityID string) ([]byte, error) { + resp, err := get(c.url+"/states/"+entityID, c.token) if err != nil { return nil, err } diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index f0952ec..732ccb6 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -19,106 +19,103 @@ func NewAlarmControlPanel(conn *websocket.Conn) *AlarmControlPanel { } // Send the alarm the command for arm away. -// Takes an entityId and an optional -// map that is translated into service_data. -func (acp AlarmControlPanel) ArmAway(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(acp.conn, entityId) +func (acp AlarmControlPanel) ArmAway(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "alarm_control_panel" req.Service = "alarm_arm_away" + req.Target.EntityID = entityID req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the alarm the command for arm away. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (acp AlarmControlPanel) ArmWithCustomBypass(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(acp.conn, entityId) +func (acp AlarmControlPanel) ArmWithCustomBypass(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "alarm_control_panel" req.Service = "alarm_arm_custom_bypass" + req.Target.EntityID = entityID req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the alarm the command for arm home. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (acp AlarmControlPanel) ArmHome(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(acp.conn, entityId) +func (acp AlarmControlPanel) ArmHome(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "alarm_control_panel" req.Service = "alarm_arm_home" + req.Target.EntityID = entityID req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(acp.conn, entityId) +func (acp AlarmControlPanel) ArmNight(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "alarm_control_panel" req.Service = "alarm_arm_night" + req.Target.EntityID = entityID req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(acp.conn, entityId) +func (acp AlarmControlPanel) ArmVacation(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "alarm_control_panel" req.Service = "alarm_arm_vacation" + req.Target.EntityID = entityID req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(acp.conn, entityId) +func (acp AlarmControlPanel) Disarm(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "alarm_control_panel" req.Service = "alarm_disarm" + req.Target.EntityID = entityID req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(acp.conn, entityId) +func (acp AlarmControlPanel) Trigger(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "alarm_control_panel" req.Service = "alarm_trigger" + req.Target.EntityID = entityID req.ServiceData = serviceData acp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/climate.go b/internal/services/climate.go index b6b8d78..0700818 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -19,26 +19,28 @@ func NewClimate(conn *websocket.Conn) *Climate { /* Public API */ -func (c Climate) SetFanMode(entityId string, fanMode string) { - req := NewBaseServiceRequest(c.conn, entityId) +func (c Climate) SetFanMode(entityID string, fanMode string) { + req := CallServiceRequest{} req.Domain = "climate" req.Service = "set_fan_mode" + req.Target.EntityID = entityID req.ServiceData = map[string]any{"fan_mode": fanMode} c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (c Climate) SetTemperature(entityId string, serviceData types.SetTemperatureRequest) { - req := NewBaseServiceRequest(c.conn, entityId) +func (c Climate) SetTemperature(entityID string, serviceData types.SetTemperatureRequest) { + req := CallServiceRequest{} req.Domain = "climate" req.Service = "set_temperature" + req.Target.EntityID = entityID req.ServiceData = serviceData.ToJSON() c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/cover.go b/internal/services/cover.go index 828ceef..d9a58b6 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -18,126 +18,136 @@ func NewCover(conn *websocket.Conn) *Cover { /* Public API */ -// Close all or specified cover. Takes an entityId. -func (c Cover) Close(entityId string) { - req := NewBaseServiceRequest(c.conn, entityId) +// Close all or specified cover. Takes an entityID. +func (c Cover) Close(entityID string) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "close_cover" + req.Target.EntityID = entityID c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Close all or specified cover tilt. Takes an entityId. -func (c Cover) CloseTilt(entityId string) { - req := NewBaseServiceRequest(c.conn, entityId) +// Close all or specified cover tilt. Takes an entityID. +func (c Cover) CloseTilt(entityID string) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "close_cover_tilt" + req.Target.EntityID = entityID c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Open all or specified cover. Takes an entityId. -func (c Cover) Open(entityId string) { - req := NewBaseServiceRequest(c.conn, entityId) +// Open all or specified cover. Takes an entityID. +func (c Cover) Open(entityID string) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "open_cover" + req.Target.EntityID = entityID c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Open all or specified cover tilt. Takes an entityId. -func (c Cover) OpenTilt(entityId string) { - req := NewBaseServiceRequest(c.conn, entityId) +// Open all or specified cover tilt. Takes an entityID. +func (c Cover) OpenTilt(entityID string) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "open_cover_tilt" + req.Target.EntityID = entityID c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Move to specific position all or specified cover. Takes an entityId and an optional +// 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) { - req := NewBaseServiceRequest(c.conn, entityId) +func (c Cover) SetPosition(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "set_cover_position" + req.Target.EntityID = entityID req.ServiceData = serviceData c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Move to specific position all or specified cover tilt. Takes an entityId and an optional +// 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) { - req := NewBaseServiceRequest(c.conn, entityId) +func (c Cover) SetTiltPosition(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "set_cover_tilt_position" + req.Target.EntityID = entityID req.ServiceData = serviceData c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Stop a cover entity. Takes an entityId. -func (c Cover) Stop(entityId string) { - req := NewBaseServiceRequest(c.conn, entityId) +// Stop a cover entity. Takes an entityID. +func (c Cover) Stop(entityID string) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "stop_cover" + req.Target.EntityID = entityID c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Stop a cover entity tilt. Takes an entityId. -func (c Cover) StopTilt(entityId string) { - req := NewBaseServiceRequest(c.conn, entityId) +// Stop a cover entity tilt. Takes an entityID. +func (c Cover) StopTilt(entityID string) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "stop_cover_tilt" + req.Target.EntityID = entityID c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Toggle a cover open/closed. Takes an entityId. -func (c Cover) Toggle(entityId string) { - req := NewBaseServiceRequest(c.conn, entityId) +// Toggle a cover open/closed. Takes an entityID. +func (c Cover) Toggle(entityID string) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "toggle" + req.Target.EntityID = entityID c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Toggle a cover tilt open/closed. Takes an entityId. -func (c Cover) ToggleTilt(entityId string) { - req := NewBaseServiceRequest(c.conn, entityId) +// Toggle a cover tilt open/closed. Takes an entityID. +func (c Cover) ToggleTilt(entityID string) { + req := CallServiceRequest{} req.Domain = "cover" req.Service = "toggle_cover_tilt" + req.Target.EntityID = entityID c.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/event.go b/internal/services/event.go index 61827e9..44d4bcb 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -16,7 +16,7 @@ func NewEvent(conn *websocket.Conn) *Event { // Fire an event type FireEventRequest struct { - Id int64 `json:"id"` + 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"` @@ -35,7 +35,7 @@ func (e Event) Fire(eventType string, eventData map[string]any) { req.EventData = eventData e.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 8f013f5..58083af 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -14,41 +14,44 @@ func NewHomeAssistant(conn *websocket.Conn) *HomeAssistant { } } -// TurnOn a Home Assistant entity. Takes an entityId and an optional +// 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) { - req := NewBaseServiceRequest(ha.conn, entityId) +func (ha *HomeAssistant) TurnOn(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "homeassistant" req.Service = "turn_on" + req.Target.EntityID = entityID req.ServiceData = serviceData ha.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Toggle a Home Assistant entity. Takes an entityId and an optional +// 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) { - req := NewBaseServiceRequest(ha.conn, entityId) +func (ha *HomeAssistant) Toggle(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "homeassistant" req.Service = "toggle" + req.Target.EntityID = entityID req.ServiceData = serviceData ha.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (ha *HomeAssistant) TurnOff(entityId string) { - req := NewBaseServiceRequest(ha.conn, entityId) +func (ha *HomeAssistant) TurnOff(entityID string) { + req := CallServiceRequest{} req.Domain = "homeassistant" req.Service = "turn_off" + req.Target.EntityID = entityID ha.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 95cf100..37ad2df 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -18,46 +18,49 @@ func NewInputBoolean(conn *websocket.Conn) *InputBoolean { /* Public API */ -func (ib InputBoolean) TurnOn(entityId string) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputBoolean) TurnOn(entityID string) { + req := CallServiceRequest{} req.Domain = "input_boolean" req.Service = "turn_on" + req.Target.EntityID = entityID ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (ib InputBoolean) Toggle(entityId string) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputBoolean) Toggle(entityID string) { + req := CallServiceRequest{} req.Domain = "input_boolean" req.Service = "toggle" + req.Target.EntityID = entityID ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (ib InputBoolean) TurnOff(entityId string) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputBoolean) TurnOff(entityID string) { + req := CallServiceRequest{} req.Domain = "input_boolean" req.Service = "turn_off" + req.Target.EntityID = entityID ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } func (ib InputBoolean) Reload() { - req := NewBaseServiceRequest(ib.conn, "") + req := CallServiceRequest{} req.Domain = "input_boolean" req.Service = "reload" ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 58ed020..e397faf 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -18,24 +18,25 @@ func NewInputButton(conn *websocket.Conn) *InputButton { /* Public API */ -func (ib InputButton) Press(entityId string) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputButton) Press(entityID string) { + req := CallServiceRequest{} req.Domain = "input_button" req.Service = "press" + req.Target.EntityID = entityID ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } func (ib InputButton) Reload() { - req := NewBaseServiceRequest(ib.conn, "") + req := CallServiceRequest{} req.Domain = "input_button" req.Service = "reload" ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index e6de5db..1f512b4 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -21,27 +21,28 @@ func NewInputDatetime(conn *websocket.Conn) *InputDatetime { /* Public API */ -func (ib InputDatetime) Set(entityId string, value time.Time) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputDatetime) Set(entityID string, value time.Time) { + req := CallServiceRequest{} req.Domain = "input_datetime" req.Service = "set_datetime" + req.Target.EntityID = entityID req.ServiceData = map[string]any{ "timestamp": fmt.Sprint(value.Unix()), } ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } func (ib InputDatetime) Reload() { - req := NewBaseServiceRequest(ib.conn, "") + req := CallServiceRequest{} req.Domain = "input_datetime" req.Service = "reload" ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 4bfafc2..bddd43d 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -18,47 +18,50 @@ func NewInputNumber(conn *websocket.Conn) *InputNumber { /* Public API */ -func (ib InputNumber) Set(entityId string, value float32) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputNumber) Set(entityID string, value float32) { + req := CallServiceRequest{} req.Domain = "input_number" req.Service = "set_value" + req.Target.EntityID = entityID req.ServiceData = map[string]any{"value": value} ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (ib InputNumber) Increment(entityId string) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputNumber) Increment(entityID string) { + req := CallServiceRequest{} req.Domain = "input_number" req.Service = "increment" + req.Target.EntityID = entityID ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (ib InputNumber) Decrement(entityId string) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputNumber) Decrement(entityID string) { + req := CallServiceRequest{} req.Domain = "input_number" req.Service = "decrement" + req.Target.EntityID = entityID ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } func (ib InputNumber) Reload() { - req := NewBaseServiceRequest(ib.conn, "") + req := CallServiceRequest{} req.Domain = "input_number" req.Service = "reload" ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 546e764..ce5550e 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -18,27 +18,28 @@ func NewInputText(conn *websocket.Conn) *InputText { /* Public API */ -func (ib InputText) Set(entityId string, value string) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib InputText) Set(entityID string, value string) { + req := CallServiceRequest{} req.Domain = "input_text" req.Service = "set_value" + req.Target.EntityID = entityID req.ServiceData = map[string]any{ "value": value, } ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } func (ib InputText) Reload() { - req := NewBaseServiceRequest(ib.conn, "") + req := CallServiceRequest{} req.Domain = "input_text" req.Service = "reload" ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/light.go b/internal/services/light.go index e3ddae2..fccec8e 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -18,41 +18,42 @@ func NewLight(conn *websocket.Conn) *Light { /* Public API */ -// 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) { - req := NewBaseServiceRequest(l.conn, entityId) +// TurnOn a light entity. +func (l Light) TurnOn(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "light" req.Service = "turn_on" + req.Target.EntityID = entityID req.ServiceData = serviceData l.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(l.conn, entityId) +// Toggle a light entity. +func (l Light) Toggle(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "light" req.Service = "toggle" + req.Target.EntityID = entityID req.ServiceData = serviceData l.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (l Light) TurnOff(entityId string) { - req := NewBaseServiceRequest(l.conn, entityId) +func (l Light) TurnOff(entityID string) { + req := CallServiceRequest{} req.Domain = "light" req.Service = "turn_off" + req.Target.EntityID = entityID l.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/lock.go b/internal/services/lock.go index b902ecf..ada7fe3 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -18,30 +18,30 @@ func NewLock(conn *websocket.Conn) *Lock { /* Public API */ -// 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) { - req := NewBaseServiceRequest(l.conn, entityId) +// Lock a lock entity. +func (l Lock) Lock(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "lock" req.Service = "lock" + req.Target.EntityID = entityID req.ServiceData = serviceData l.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(l.conn, entityId) +// Unlock a lock entity. +func (l Lock) Unlock(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "lock" req.Service = "unlock" + req.Target.EntityID = entityID req.ServiceData = serviceData l.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 0d268f6..42348cc 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -19,305 +19,327 @@ func NewMediaPlayer(conn *websocket.Conn) *MediaPlayer { /* Public API */ // Send the media player the command to clear players playlist. -// Takes an entityId. -func (mp MediaPlayer) ClearPlaylist(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) ClearPlaylist(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "clear_playlist" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Group players together. Only works on platforms with support for player groups. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (mp MediaPlayer) Join(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) Join(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "join" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the command for next track. -// Takes an entityId. -func (mp MediaPlayer) Next(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) Next(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "media_next_track" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the command for pause. -// Takes an entityId. -func (mp MediaPlayer) Pause(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) Pause(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "media_pause" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the command for play. -// Takes an entityId. -func (mp MediaPlayer) Play(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) Play(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "media_play" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Toggle media player play/pause state. -// Takes an entityId. -func (mp MediaPlayer) PlayPause(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) PlayPause(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "media_play_pause" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the command for previous track. -// Takes an entityId. -func (mp MediaPlayer) Previous(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) Previous(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "media_previous_track" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the command to seek in current playing media. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (mp MediaPlayer) Seek(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) Seek(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "media_seek" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the stop command. -// Takes an entityId. -func (mp MediaPlayer) Stop(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) Stop(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "media_stop" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the command for playing media. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (mp MediaPlayer) PlayMedia(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) PlayMedia(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "play_media" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -// Set repeat mode. Takes an entityId and an optional +// 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) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) RepeatSet(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "repeat_set" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the command to change sound mode. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (mp MediaPlayer) SelectSoundMode(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) SelectSoundMode(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "select_sound_mode" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Send the media player the command to change input source. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (mp MediaPlayer) SelectSource(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) SelectSource(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "select_source" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Set shuffling state. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (mp MediaPlayer) Shuffle(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) Shuffle(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "shuffle_set" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Toggles a media player power state. -// Takes an entityId. -func (mp MediaPlayer) Toggle(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) Toggle(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "toggle" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Turn a media player power off. -// Takes an entityId. -func (mp MediaPlayer) TurnOff(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) TurnOff(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "turn_off" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Turn a media player power on. -// Takes an entityId. -func (mp MediaPlayer) TurnOn(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) TurnOn(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "turn_on" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) Unjoin(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "unjoin" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Turn a media player volume down. -// Takes an entityId. -func (mp MediaPlayer) VolumeDown(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) VolumeDown(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "volume_down" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Mute a media player's volume. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (mp MediaPlayer) VolumeMute(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) VolumeMute(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "volume_mute" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Set a media player's volume level. -// Takes an entityId and an optional +// Takes an entityID and an optional // map that is translated into service_data. -func (mp MediaPlayer) VolumeSet(entityId string, serviceData map[string]any) { - req := NewBaseServiceRequest(mp.conn, entityId) +func (mp MediaPlayer) VolumeSet(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "volume_set" + req.Target.EntityID = entityID req.ServiceData = serviceData mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Turn a media player volume up. -// Takes an entityId. -func (mp MediaPlayer) VolumeUp(entityId string) { - req := NewBaseServiceRequest(mp.conn, entityId) +// Takes an entityID. +func (mp MediaPlayer) VolumeUp(entityID string) { + req := CallServiceRequest{} req.Domain = "media_player" req.Service = "volume_up" + req.Target.EntityID = entityID mp.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/notify.go b/internal/services/notify.go index b1320b4..9b0bcc2 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -17,7 +17,7 @@ func NewNotify(conn *websocket.Conn) *Notify { // Send a notification. Takes a types.NotifyRequest. func (ha *Notify) Notify(reqData types.NotifyRequest) { - req := NewBaseServiceRequest(ha.conn, "") + req := CallServiceRequest{} req.Domain = "notify" req.Service = reqData.ServiceName @@ -31,7 +31,7 @@ func (ha *Notify) Notify(reqData types.NotifyRequest) { req.ServiceData = serviceData ha.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/number.go b/internal/services/number.go index c94e69f..c3be8b8 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -18,14 +18,15 @@ func NewNumber(conn *websocket.Conn) *Number { /* Public API */ -func (ib Number) SetValue(entityId string, value float32) { - req := NewBaseServiceRequest(ib.conn, entityId) +func (ib Number) SetValue(entityID string, value float32) { + req := CallServiceRequest{} req.Domain = "number" req.Service = "set_value" + req.Target.EntityID = entityID req.ServiceData = map[string]any{"value": value} ib.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/scene.go b/internal/services/scene.go index cb30dd3..73bd8e3 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -20,53 +20,53 @@ func NewScene(conn *websocket.Conn) *Scene { // Apply a scene. Takes map that is translated into service_data. func (s Scene) Apply(serviceData map[string]any) { - req := NewBaseServiceRequest(s.conn, "") + req := CallServiceRequest{} req.Domain = "scene" req.Service = "apply" req.ServiceData = serviceData s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(s.conn, entityId) +// Create a scene entity. +func (s Scene) Create(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "scene" req.Service = "create" + req.Target.EntityID = entityID req.ServiceData = serviceData s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Reload the scenes. func (s Scene) Reload() { - req := NewBaseServiceRequest(s.conn, "") + req := CallServiceRequest{} req.Domain = "scene" req.Service = "reload" s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(s.conn, entityId) +// TurnOn a scene entity. +func (s Scene) TurnOn(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "scene" req.Service = "turn_on" + req.Target.EntityID = entityID req.ServiceData = serviceData s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/script.go b/internal/services/script.go index 5d431df..b017462 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -19,49 +19,52 @@ func NewScript(conn *websocket.Conn) *Script { /* Public API */ // Reload a script that was created in the HA UI. -func (s Script) Reload(entityId string) { - req := NewBaseServiceRequest(s.conn, entityId) +func (s Script) Reload(entityID string) { + req := CallServiceRequest{} req.Domain = "script" req.Service = "reload" + req.Target.EntityID = entityID s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Toggle a script that was created in the HA UI. -func (s Script) Toggle(entityId string) { - req := NewBaseServiceRequest(s.conn, entityId) +func (s Script) Toggle(entityID string) { + req := CallServiceRequest{} req.Domain = "script" req.Service = "toggle" + req.Target.EntityID = entityID s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Turn off a script that was created in the HA UI. func (s Script) TurnOff() { - req := NewBaseServiceRequest(s.conn, "") + req := CallServiceRequest{} req.Domain = "script" req.Service = "turn_off" s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Turn on a script that was created in the HA UI. -func (s Script) TurnOn(entityId string) { - req := NewBaseServiceRequest(s.conn, entityId) +func (s Script) TurnOn(entityID string) { + req := CallServiceRequest{} req.Domain = "script" req.Service = "turn_on" + req.Target.EntityID = entityID s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/services.go b/internal/services/services.go index 2b6dde8..e5ae32d 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -1,26 +1,23 @@ package services -import ( - "saml.dev/gome-assistant/internal/websocket" -) +// CallService is a type that always serializes as `"call_service"`. +type CallService struct{} -type BaseServiceRequest struct { - Id int64 `json:"id"` - RequestType string `json:"type"` // hardcoded "call_service" +func (CallService) String() string { + return "call_service" +} + +func (CallService) MarshalJSON() ([]byte, error) { + return []byte(`"call_service"`), nil +} + +type CallServiceRequest struct { + ID int64 `json:"id"` + RequestType CallService `json:"type"` // hardcoded "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"` + EntityID string `json:"entity_id,omitempty"` } `json:"target,omitempty"` } - -func NewBaseServiceRequest(conn *websocket.Conn, entityId string) BaseServiceRequest { - bsr := BaseServiceRequest{ - RequestType: "call_service", - } - if entityId != "" { - bsr.Target.EntityId = entityId - } - return bsr -} diff --git a/internal/services/switch.go b/internal/services/switch.go index d9aaed6..46ca452 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -18,35 +18,38 @@ func NewSwitch(conn *websocket.Conn) *Switch { /* Public API */ -func (s Switch) TurnOn(entityId string) { - req := NewBaseServiceRequest(s.conn, entityId) +func (s Switch) TurnOn(entityID string) { + req := CallServiceRequest{} req.Domain = "switch" req.Service = "turn_on" + req.Target.EntityID = entityID s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (s Switch) Toggle(entityId string) { - req := NewBaseServiceRequest(s.conn, entityId) +func (s Switch) Toggle(entityID string) { + req := CallServiceRequest{} req.Domain = "switch" req.Service = "toggle" + req.Target.EntityID = entityID s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } -func (s Switch) TurnOff(entityId string) { - req := NewBaseServiceRequest(s.conn, entityId) +func (s Switch) TurnOff(entityID string) { + req := CallServiceRequest{} req.Domain = "switch" req.Service = "turn_off" + req.Target.EntityID = entityID s.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/tts.go b/internal/services/tts.go index bb322e8..9f0a8be 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -20,42 +20,41 @@ func NewTTS(conn *websocket.Conn) *TTS { // Remove all text-to-speech cache files and RAM cache. func (tts TTS) ClearCache() { - req := NewBaseServiceRequest(tts.conn, "") + req := CallServiceRequest{} req.Domain = "tts" req.Service = "clear_cache" tts.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(tts.conn, entityId) +func (tts TTS) CloudSay(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "tts" req.Service = "cloud_say" + req.Target.EntityID = entityID req.ServiceData = serviceData tts.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(tts.conn, entityId) +// Say something using text-to-speech on a media player with +// google_translate. +func (tts TTS) GoogleTranslateSay(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "tts" req.Service = "google_translate_say" + req.Target.EntityID = entityID req.ServiceData = serviceData tts.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 906bd78..5dea65c 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -19,146 +19,146 @@ func NewVacuum(conn *websocket.Conn) *Vacuum { /* Public API */ // Tell the vacuum cleaner to do a spot clean-up. -// Takes an entityId. -func (v Vacuum) CleanSpot(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) CleanSpot(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "clean_spot" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Locate the vacuum cleaner robot. -// Takes an entityId. -func (v Vacuum) Locate(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) Locate(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "locate" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Pause the cleaning task. -// Takes an entityId. -func (v Vacuum) Pause(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) Pause(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "pause" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Tell the vacuum cleaner to return to its dock. -// Takes an entityId. -func (v Vacuum) ReturnToBase(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) ReturnToBase(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "return_to_base" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(v.conn, entityId) +// Send a raw command to the vacuum cleaner. +func (v Vacuum) SendCommand(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "send_command" + req.Target.EntityID = entityID req.ServiceData = serviceData v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(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) { - req := NewBaseServiceRequest(v.conn, entityId) +// Set the fan speed of the vacuum cleaner. +func (v Vacuum) SetFanSpeed(entityID string, serviceData map[string]any) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "set_fan_speed" + req.Target.EntityID = entityID req.ServiceData = serviceData v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Start or resume the cleaning task. -// Takes an entityId. -func (v Vacuum) Start(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) Start(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "start" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Start, pause, or resume the cleaning task. -// Takes an entityId. -func (v Vacuum) StartPause(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) StartPause(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "start_pause" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Stop the current cleaning task. -// Takes an entityId. -func (v Vacuum) Stop(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) Stop(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "stop" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Stop the current cleaning task and return to home. -// Takes an entityId. -func (v Vacuum) TurnOff(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) TurnOff(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "turn_off" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } // Start a new cleaning task. -// Takes an entityId. -func (v Vacuum) TurnOn(entityId string) { - req := NewBaseServiceRequest(v.conn, entityId) +func (v Vacuum) TurnOn(entityID string) { + req := CallServiceRequest{} req.Domain = "vacuum" req.Service = "turn_on" + req.Target.EntityID = entityID v.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 58d892b..7bcbf7a 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -19,17 +19,18 @@ func NewZWaveJS(conn *websocket.Conn) *ZWaveJS { /* Public API */ // ZWaveJS bulk_set_partial_config_parameters service. -func (zw ZWaveJS) BulkSetPartialConfigParam(entityId string, parameter int, value any) { - req := NewBaseServiceRequest(zw.conn, entityId) +func (zw ZWaveJS) BulkSetPartialConfigParam(entityID string, parameter int, value any) { + req := CallServiceRequest{} req.Domain = "zwave_js" req.Service = "bulk_set_partial_config_parameters" + req.Target.EntityID = entityID req.ServiceData = map[string]any{ "parameter": parameter, "value": value, } zw.conn.Send(func(mw websocket.MessageWriter) error { - req.Id = mw.NextID() + req.ID = mw.NextID() return mw.SendMessage(req) }) } diff --git a/internal/websocket/read.go b/internal/websocket/read.go index 04a907e..b2463bd 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -8,13 +8,13 @@ import ( type BaseMessage struct { Type string `json:"type"` - Id int64 `json:"id"` + ID int64 `json:"id"` Success bool `json:"success"` } type ChanMsg struct { Type string - Id int64 + ID int64 Success bool Raw []byte } @@ -39,7 +39,7 @@ func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { } type SubEvent struct { - Id int64 `json:"id"` + ID int64 `json:"id"` Type string `json:"type"` EventType string `json:"event_type"` } @@ -57,7 +57,7 @@ func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscrip var subscription Subscription err := conn.Send(func(mw MessageWriter) error { subscription = mw.Subscribe(subscriber) - e.Id = subscription.ID() + e.ID = subscription.ID() if err := mw.SendMessage(e); err != nil { conn.unsubscribe(subscription.ID()) return fmt.Errorf("error writing to websocket: %w", err) @@ -73,7 +73,7 @@ func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscrip } type UnsubEvent struct { - Id int64 `json:"id"` + ID int64 `json:"id"` Type string `json:"type"` Subscription int64 `json:"subscription"` } @@ -87,7 +87,7 @@ func (conn *Conn) unwatchEvents(subscriptionID int64) error { } err := conn.Send(func(mw MessageWriter) error { - e.Id = mw.NextID() + e.ID = mw.NextID() return mw.SendMessage(e) }) if err != nil { @@ -124,12 +124,12 @@ func (conn *Conn) Start() { } chanMsg := ChanMsg{ Type: base.Type, - Id: base.Id, + ID: base.ID, Success: base.Success, Raw: bytes, } - if subscriber, ok := conn.getSubscriber(chanMsg.Id); ok { + if subscriber, ok := conn.getSubscriber(chanMsg.ID); ok { subscriber(chanMsg) } } diff --git a/interval.go b/interval.go index 0ba977f..07467c6 100644 --- a/interval.go +++ b/interval.go @@ -109,22 +109,22 @@ func (ib intervalBuilderEnd) ExceptionRange(start, end time.Time) intervalBuilde return ib } -// Enable this interval only when the current state of {entityId} +// Enable this interval only when the current state of {entityID} // matches {state}. If there is a network error while retrieving // state, the interval runs if {runOnNetworkError} is true. func (ib intervalBuilderEnd) EnabledWhen( - entityId, state string, runOnNetworkError bool, + entityID, state string, runOnNetworkError bool, ) intervalBuilderEnd { - if entityId == "" { + if entityID == "" { panic( fmt.Sprintf( - "entityId is empty in EnabledWhen entityId='%s' state='%s'", - entityId, state, + "entityID is empty in EnabledWhen entityID='%s' state='%s'", + entityID, state, ), ) } i := internal.EnabledDisabledInfo{ - Entity: entityId, + Entity: entityID, State: state, RunOnError: runOnNetworkError, } @@ -132,22 +132,22 @@ func (ib intervalBuilderEnd) EnabledWhen( return ib } -// Disable this interval when the current state of {entityId} matches +// Disable this interval when the current state of {entityID} matches // {state}. If there is a network error while retrieving state, the // interval runs if {runOnNetworkError} is true. func (ib intervalBuilderEnd) DisabledWhen( - entityId, state string, runOnNetworkError bool, + entityID, state string, runOnNetworkError bool, ) intervalBuilderEnd { - if entityId == "" { + if entityID == "" { panic( fmt.Sprintf( - "entityId is empty in EnabledWhen entityId='%s' state='%s'", - entityId, state, + "entityID is empty in EnabledWhen entityID='%s' state='%s'", + entityID, state, ), ) } i := internal.EnabledDisabledInfo{ - Entity: entityId, + Entity: entityID, State: state, RunOnError: runOnNetworkError, } diff --git a/schedule.go b/schedule.go index 9590144..cf6eb37 100644 --- a/schedule.go +++ b/schedule.go @@ -112,22 +112,22 @@ func (sb scheduleBuilderEnd) OnlyOnDates(t time.Time, tl ...time.Time) scheduleB return sb } -// Enable this schedule only when the current state of {entityId} +// Enable this schedule only when the current state of {entityID} // matches {state}. If there is a network error while retrieving // state, the schedule runs if {runOnNetworkError} is true. func (sb scheduleBuilderEnd) EnabledWhen( - entityId, state string, runOnNetworkError bool, + entityID, state string, runOnNetworkError bool, ) scheduleBuilderEnd { - if entityId == "" { + if entityID == "" { panic( fmt.Sprintf( - "entityId is empty in EnabledWhen entityId='%s' state='%s'", - entityId, state, + "entityID is empty in EnabledWhen entityID='%s' state='%s'", + entityID, state, ), ) } i := internal.EnabledDisabledInfo{ - Entity: entityId, + Entity: entityID, State: state, RunOnError: runOnNetworkError, } @@ -135,22 +135,22 @@ func (sb scheduleBuilderEnd) EnabledWhen( return sb } -// Disable this schedule when the current state of {entityId} matches +// Disable this schedule when the current state of {entityID} matches // {state}. If there is a network error while retrieving state, the // schedule runs if {runOnNetworkError} is true. func (sb scheduleBuilderEnd) DisabledWhen( - entityId, state string, runOnNetworkError bool, + entityID, state string, runOnNetworkError bool, ) scheduleBuilderEnd { - if entityId == "" { + if entityID == "" { panic( fmt.Sprintf( - "entityId is empty in EnabledWhen entityId='%s' state='%s'", - entityId, state, + "entityID is empty in EnabledWhen entityID='%s' state='%s'", + entityID, state, ), ) } i := internal.EnabledDisabledInfo{ - Entity: entityId, + Entity: entityID, State: state, RunOnError: runOnNetworkError, } diff --git a/state.go b/state.go index 369422d..858988c 100644 --- a/state.go +++ b/state.go @@ -15,8 +15,8 @@ type State interface { BeforeSunrise(...DurationString) bool AfterSunset(...DurationString) bool BeforeSunset(...DurationString) bool - Get(entityId string) (EntityState, error) - Equals(entityId, state string) (bool, error) + Get(entityID string) (EntityState, error) + Equals(entityID, state string) (bool, error) } // State is used to retrieve state from Home Assistant. @@ -33,22 +33,22 @@ type EntityState struct { LastChanged time.Time `json:"last_changed"` } -func newState(c *http.HttpClient, homeZoneEntityId string) (*StateImpl, error) { +func newState(c *http.HttpClient, homeZoneEntityID string) (*StateImpl, error) { state := &StateImpl{httpClient: c} - err := state.getLatLong(c, homeZoneEntityId) + err := state.getLatLong(c, homeZoneEntityID) if err != nil { return nil, err } return state, nil } -func (s *StateImpl) getLatLong(c *http.HttpClient, homeZoneEntityId string) error { - resp, err := s.Get(homeZoneEntityId) +func (s *StateImpl) getLatLong(c *http.HttpClient, homeZoneEntityID string) error { + resp, err := s.Get(homeZoneEntityID) if err != nil { return fmt.Errorf( "couldn't get latitude/longitude from home assistant entity '%s'. "+ "Did you type it correctly? It should be a zone like 'zone.home'", - homeZoneEntityId, + homeZoneEntityID, ) } @@ -67,8 +67,8 @@ func (s *StateImpl) getLatLong(c *http.HttpClient, homeZoneEntityId string) erro return nil } -func (s *StateImpl) Get(entityId string) (EntityState, error) { - resp, err := s.httpClient.GetState(entityId) +func (s *StateImpl) Get(entityID string) (EntityState, error) { + resp, err := s.httpClient.GetState(entityID) if err != nil { return EntityState{}, err } @@ -77,8 +77,8 @@ func (s *StateImpl) Get(entityId string) (EntityState, error) { return es, nil } -func (s *StateImpl) Equals(entityId string, expectedState string) (bool, error) { - currentState, err := s.Get(entityId) +func (s *StateImpl) Equals(entityID string, expectedState string) (bool, error) { + currentState, err := s.Get(entityID) if err != nil { return false, err } From d349cd0bb0ad67a72a22803796e8bedbf30675cf Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 20 Apr 2024 12:49:44 +0200 Subject: [PATCH 040/103] Initialize `CallServiceRequest` objects while defining them --- internal/services/alarm_control_panel.go | 91 +++++--- internal/services/climate.go | 26 ++- internal/services/cover.go | 114 ++++++---- internal/services/homeassistant.go | 37 ++-- internal/services/input_boolean.go | 40 ++-- internal/services/input_button.go | 18 +- internal/services/input_datetime.go | 22 +- internal/services/input_number.go | 42 ++-- internal/services/input_text.go | 22 +- internal/services/light.go | 37 ++-- internal/services/lock.go | 26 ++- internal/services/media_player.go | 261 ++++++++++++++--------- internal/services/notify.go | 19 +- internal/services/number.go | 13 +- internal/services/scene.go | 42 ++-- internal/services/script.go | 40 ++-- internal/services/services.go | 9 +- internal/services/switch.go | 33 +-- internal/services/tts.go | 33 +-- internal/services/vacuum.go | 125 +++++++---- internal/services/zwavejs.go | 17 +- 21 files changed, 661 insertions(+), 406 deletions(-) diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 732ccb6..7eed9eb 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -20,11 +20,14 @@ func NewAlarmControlPanel(conn *websocket.Conn) *AlarmControlPanel { // Send the alarm the command for arm away. func (acp AlarmControlPanel) ArmAway(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_away" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_away", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } acp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -36,11 +39,14 @@ func (acp AlarmControlPanel) ArmAway(entityID string, serviceData map[string]any // Takes an entityID and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmWithCustomBypass(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_custom_bypass" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_custom_bypass", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } acp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -52,11 +58,14 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityID string, serviceData ma // Takes an entityID and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmHome(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_home" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_home", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } acp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -66,11 +75,14 @@ func (acp AlarmControlPanel) ArmHome(entityID string, serviceData map[string]any // Send the alarm the command for arm night. func (acp AlarmControlPanel) ArmNight(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_night" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_night", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } acp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -80,11 +92,14 @@ func (acp AlarmControlPanel) ArmNight(entityID string, serviceData map[string]an // Send the alarm the command for arm vacation. func (acp AlarmControlPanel) ArmVacation(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "alarm_control_panel" - req.Service = "alarm_arm_vacation" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_arm_vacation", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } acp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -94,11 +109,14 @@ func (acp AlarmControlPanel) ArmVacation(entityID string, serviceData map[string // Send the alarm the command for disarm. func (acp AlarmControlPanel) Disarm(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "alarm_control_panel" - req.Service = "alarm_disarm" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_disarm", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } acp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -108,11 +126,14 @@ func (acp AlarmControlPanel) Disarm(entityID string, serviceData map[string]any) // Send the alarm the command for trigger. func (acp AlarmControlPanel) Trigger(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "alarm_control_panel" - req.Service = "alarm_trigger" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "alarm_control_panel", + Service: "alarm_trigger", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } acp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/climate.go b/internal/services/climate.go index 0700818..d5354dd 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -20,11 +20,14 @@ func NewClimate(conn *websocket.Conn) *Climate { /* Public API */ func (c Climate) SetFanMode(entityID string, fanMode string) { - req := CallServiceRequest{} - req.Domain = "climate" - req.Service = "set_fan_mode" - req.Target.EntityID = entityID - req.ServiceData = map[string]any{"fan_mode": fanMode} + req := CallServiceRequest{ + Domain: "climate", + Service: "set_fan_mode", + Target: Target{ + EntityID: entityID, + }, + ServiceData: map[string]any{"fan_mode": fanMode}, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -33,11 +36,14 @@ func (c Climate) SetFanMode(entityID string, fanMode string) { } func (c Climate) SetTemperature(entityID string, serviceData types.SetTemperatureRequest) { - req := CallServiceRequest{} - req.Domain = "climate" - req.Service = "set_temperature" - req.Target.EntityID = entityID - req.ServiceData = serviceData.ToJSON() + req := CallServiceRequest{ + Domain: "climate", + Service: "set_temperature", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData.ToJSON(), + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/cover.go b/internal/services/cover.go index d9a58b6..7437b17 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -20,10 +20,13 @@ func NewCover(conn *websocket.Conn) *Cover { // Close all or specified cover. Takes an entityID. func (c Cover) Close(entityID string) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "close_cover" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "cover", + Service: "close_cover", + Target: Target{ + EntityID: entityID, + }, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -33,10 +36,13 @@ func (c Cover) Close(entityID string) { // Close all or specified cover tilt. Takes an entityID. func (c Cover) CloseTilt(entityID string) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "close_cover_tilt" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "cover", + Service: "close_cover_tilt", + Target: Target{ + EntityID: entityID, + }, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -46,10 +52,13 @@ func (c Cover) CloseTilt(entityID string) { // Open all or specified cover. Takes an entityID. func (c Cover) Open(entityID string) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "open_cover" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "cover", + Service: "open_cover", + Target: Target{ + EntityID: entityID, + }, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -59,10 +68,13 @@ func (c Cover) Open(entityID string) { // Open all or specified cover tilt. Takes an entityID. func (c Cover) OpenTilt(entityID string) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "open_cover_tilt" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "cover", + Service: "open_cover_tilt", + Target: Target{ + EntityID: entityID, + }, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -73,11 +85,14 @@ func (c Cover) OpenTilt(entityID string) { // 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) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "set_cover_position" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "cover", + Service: "set_cover_position", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -88,11 +103,14 @@ func (c Cover) SetPosition(entityID string, serviceData map[string]any) { // 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) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "set_cover_tilt_position" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "cover", + Service: "set_cover_tilt_position", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -102,10 +120,13 @@ func (c Cover) SetTiltPosition(entityID string, serviceData map[string]any) { // Stop a cover entity. Takes an entityID. func (c Cover) Stop(entityID string) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "stop_cover" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "cover", + Service: "stop_cover", + Target: Target{ + EntityID: entityID, + }, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -115,10 +136,13 @@ func (c Cover) Stop(entityID string) { // Stop a cover entity tilt. Takes an entityID. func (c Cover) StopTilt(entityID string) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "stop_cover_tilt" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "cover", + Service: "stop_cover_tilt", + Target: Target{ + EntityID: entityID, + }, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -128,10 +152,13 @@ func (c Cover) StopTilt(entityID string) { // Toggle a cover open/closed. Takes an entityID. func (c Cover) Toggle(entityID string) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "toggle" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "cover", + Service: "toggle", + Target: Target{ + EntityID: entityID, + }, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -141,10 +168,13 @@ func (c Cover) Toggle(entityID string) { // Toggle a cover tilt open/closed. Takes an entityID. func (c Cover) ToggleTilt(entityID string) { - req := CallServiceRequest{} - req.Domain = "cover" - req.Service = "toggle_cover_tilt" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "cover", + Service: "toggle_cover_tilt", + Target: Target{ + EntityID: entityID, + }, + } c.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 58083af..aac4aaa 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -17,11 +17,14 @@ func NewHomeAssistant(conn *websocket.Conn) *HomeAssistant { // 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) { - req := CallServiceRequest{} - req.Domain = "homeassistant" - req.Service = "turn_on" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "homeassistant", + Service: "turn_on", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } ha.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -32,11 +35,14 @@ func (ha *HomeAssistant) TurnOn(entityID string, serviceData map[string]any) { // 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) { - req := CallServiceRequest{} - req.Domain = "homeassistant" - req.Service = "toggle" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "homeassistant", + Service: "toggle", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } ha.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -45,10 +51,13 @@ func (ha *HomeAssistant) Toggle(entityID string, serviceData map[string]any) { } func (ha *HomeAssistant) TurnOff(entityID string) { - req := CallServiceRequest{} - req.Domain = "homeassistant" - req.Service = "turn_off" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "homeassistant", + Service: "turn_off", + Target: Target{ + EntityID: entityID, + }, + } ha.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 37ad2df..662cba5 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -19,10 +19,13 @@ func NewInputBoolean(conn *websocket.Conn) *InputBoolean { /* Public API */ func (ib InputBoolean) TurnOn(entityID string) { - req := CallServiceRequest{} - req.Domain = "input_boolean" - req.Service = "turn_on" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "input_boolean", + Service: "turn_on", + Target: Target{ + EntityID: entityID, + }, + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -31,10 +34,13 @@ func (ib InputBoolean) TurnOn(entityID string) { } func (ib InputBoolean) Toggle(entityID string) { - req := CallServiceRequest{} - req.Domain = "input_boolean" - req.Service = "toggle" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "input_boolean", + Service: "toggle", + Target: Target{ + EntityID: entityID, + }, + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -43,10 +49,13 @@ func (ib InputBoolean) Toggle(entityID string) { } func (ib InputBoolean) TurnOff(entityID string) { - req := CallServiceRequest{} - req.Domain = "input_boolean" - req.Service = "turn_off" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "input_boolean", + Service: "turn_off", + Target: Target{ + EntityID: entityID, + }, + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -55,9 +64,10 @@ func (ib InputBoolean) TurnOff(entityID string) { } func (ib InputBoolean) Reload() { - req := CallServiceRequest{} - req.Domain = "input_boolean" - req.Service = "reload" + req := CallServiceRequest{ + Domain: "input_boolean", + Service: "reload", + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/input_button.go b/internal/services/input_button.go index e397faf..e50d5a0 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -19,10 +19,13 @@ func NewInputButton(conn *websocket.Conn) *InputButton { /* Public API */ func (ib InputButton) Press(entityID string) { - req := CallServiceRequest{} - req.Domain = "input_button" - req.Service = "press" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "input_button", + Service: "press", + Target: Target{ + EntityID: entityID, + }, + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -31,9 +34,10 @@ func (ib InputButton) Press(entityID string) { } func (ib InputButton) Reload() { - req := CallServiceRequest{} - req.Domain = "input_button" - req.Service = "reload" + req := CallServiceRequest{ + Domain: "input_button", + Service: "reload", + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 1f512b4..f609a40 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -22,12 +22,15 @@ func NewInputDatetime(conn *websocket.Conn) *InputDatetime { /* Public API */ func (ib InputDatetime) Set(entityID string, value time.Time) { - req := CallServiceRequest{} - req.Domain = "input_datetime" - req.Service = "set_datetime" - req.Target.EntityID = entityID - req.ServiceData = map[string]any{ - "timestamp": fmt.Sprint(value.Unix()), + req := CallServiceRequest{ + Domain: "input_datetime", + Service: "set_datetime", + Target: Target{ + EntityID: entityID, + }, + ServiceData: map[string]any{ + "timestamp": fmt.Sprint(value.Unix()), + }, } ib.conn.Send(func(mw websocket.MessageWriter) error { @@ -37,9 +40,10 @@ func (ib InputDatetime) Set(entityID string, value time.Time) { } func (ib InputDatetime) Reload() { - req := CallServiceRequest{} - req.Domain = "input_datetime" - req.Service = "reload" + req := CallServiceRequest{ + Domain: "input_datetime", + Service: "reload", + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/input_number.go b/internal/services/input_number.go index bddd43d..297eda8 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -19,11 +19,14 @@ func NewInputNumber(conn *websocket.Conn) *InputNumber { /* Public API */ func (ib InputNumber) Set(entityID string, value float32) { - req := CallServiceRequest{} - req.Domain = "input_number" - req.Service = "set_value" - req.Target.EntityID = entityID - req.ServiceData = map[string]any{"value": value} + req := CallServiceRequest{ + Domain: "input_number", + Service: "set_value", + Target: Target{ + EntityID: entityID, + }, + ServiceData: map[string]any{"value": value}, + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -32,10 +35,13 @@ func (ib InputNumber) Set(entityID string, value float32) { } func (ib InputNumber) Increment(entityID string) { - req := CallServiceRequest{} - req.Domain = "input_number" - req.Service = "increment" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "input_number", + Service: "increment", + Target: Target{ + EntityID: entityID, + }, + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -44,10 +50,13 @@ func (ib InputNumber) Increment(entityID string) { } func (ib InputNumber) Decrement(entityID string) { - req := CallServiceRequest{} - req.Domain = "input_number" - req.Service = "decrement" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "input_number", + Service: "decrement", + Target: Target{ + EntityID: entityID, + }, + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -56,9 +65,10 @@ func (ib InputNumber) Decrement(entityID string) { } func (ib InputNumber) Reload() { - req := CallServiceRequest{} - req.Domain = "input_number" - req.Service = "reload" + req := CallServiceRequest{ + Domain: "input_number", + Service: "reload", + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/input_text.go b/internal/services/input_text.go index ce5550e..ae593b0 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -19,12 +19,15 @@ func NewInputText(conn *websocket.Conn) *InputText { /* Public API */ func (ib InputText) Set(entityID string, value string) { - req := CallServiceRequest{} - req.Domain = "input_text" - req.Service = "set_value" - req.Target.EntityID = entityID - req.ServiceData = map[string]any{ - "value": value, + req := CallServiceRequest{ + Domain: "input_text", + Service: "set_value", + Target: Target{ + EntityID: entityID, + }, + ServiceData: map[string]any{ + "value": value, + }, } ib.conn.Send(func(mw websocket.MessageWriter) error { @@ -34,9 +37,10 @@ func (ib InputText) Set(entityID string, value string) { } func (ib InputText) Reload() { - req := CallServiceRequest{} - req.Domain = "input_text" - req.Service = "reload" + req := CallServiceRequest{ + Domain: "input_text", + Service: "reload", + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/light.go b/internal/services/light.go index fccec8e..bf54848 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -20,11 +20,14 @@ func NewLight(conn *websocket.Conn) *Light { // TurnOn a light entity. func (l Light) TurnOn(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "light" - req.Service = "turn_on" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "light", + Service: "turn_on", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } l.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -34,11 +37,14 @@ func (l Light) TurnOn(entityID string, serviceData map[string]any) { // Toggle a light entity. func (l Light) Toggle(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "light" - req.Service = "toggle" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "light", + Service: "toggle", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } l.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -47,10 +53,13 @@ func (l Light) Toggle(entityID string, serviceData map[string]any) { } func (l Light) TurnOff(entityID string) { - req := CallServiceRequest{} - req.Domain = "light" - req.Service = "turn_off" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "light", + Service: "turn_off", + Target: Target{ + EntityID: entityID, + }, + } l.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/lock.go b/internal/services/lock.go index ada7fe3..c3b665b 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -20,11 +20,14 @@ func NewLock(conn *websocket.Conn) *Lock { // Lock a lock entity. func (l Lock) Lock(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "lock" - req.Service = "lock" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "lock", + Service: "lock", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } l.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -34,11 +37,14 @@ func (l Lock) Lock(entityID string, serviceData map[string]any) { // Unlock a lock entity. func (l Lock) Unlock(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "lock" - req.Service = "unlock" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "lock", + Service: "unlock", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } l.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 42348cc..664ac3b 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -21,10 +21,13 @@ func NewMediaPlayer(conn *websocket.Conn) *MediaPlayer { // Send the media player the command to clear players playlist. // Takes an entityID. func (mp MediaPlayer) ClearPlaylist(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "clear_playlist" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "clear_playlist", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -36,11 +39,14 @@ func (mp MediaPlayer) ClearPlaylist(entityID string) { // Takes an entityID and an optional // map that is translated into service_data. func (mp MediaPlayer) Join(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "join" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "media_player", + Service: "join", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -51,10 +57,13 @@ func (mp MediaPlayer) Join(entityID string, serviceData map[string]any) { // Send the media player the command for next track. // Takes an entityID. func (mp MediaPlayer) Next(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "media_next_track" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "media_next_track", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -65,10 +74,13 @@ func (mp MediaPlayer) Next(entityID string) { // Send the media player the command for pause. // Takes an entityID. func (mp MediaPlayer) Pause(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "media_pause" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "media_pause", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -79,10 +91,13 @@ func (mp MediaPlayer) Pause(entityID string) { // Send the media player the command for play. // Takes an entityID. func (mp MediaPlayer) Play(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "media_play" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "media_play", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -93,10 +108,13 @@ func (mp MediaPlayer) Play(entityID string) { // Toggle media player play/pause state. // Takes an entityID. func (mp MediaPlayer) PlayPause(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "media_play_pause" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "media_play_pause", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -107,10 +125,13 @@ func (mp MediaPlayer) PlayPause(entityID string) { // Send the media player the command for previous track. // Takes an entityID. func (mp MediaPlayer) Previous(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "media_previous_track" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "media_previous_track", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -122,12 +143,14 @@ func (mp MediaPlayer) Previous(entityID string) { // Takes an entityID and an optional // map that is translated into service_data. func (mp MediaPlayer) Seek(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "media_seek" - req.Target.EntityID = entityID - req.ServiceData = serviceData - + req := CallServiceRequest{ + Domain: "media_player", + Service: "media_seek", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() return mw.SendMessage(req) @@ -137,10 +160,13 @@ func (mp MediaPlayer) Seek(entityID string, serviceData map[string]any) { // Send the media player the stop command. // Takes an entityID. func (mp MediaPlayer) Stop(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "media_stop" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "media_stop", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -152,11 +178,14 @@ func (mp MediaPlayer) Stop(entityID string) { // Takes an entityID and an optional // map that is translated into service_data. func (mp MediaPlayer) PlayMedia(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "play_media" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "media_player", + Service: "play_media", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -167,11 +196,14 @@ func (mp MediaPlayer) PlayMedia(entityID string, serviceData map[string]any) { // 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) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "repeat_set" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "media_player", + Service: "repeat_set", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -183,11 +215,14 @@ func (mp MediaPlayer) RepeatSet(entityID string, serviceData map[string]any) { // Takes an entityID and an optional // map that is translated into service_data. func (mp MediaPlayer) SelectSoundMode(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "select_sound_mode" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "media_player", + Service: "select_sound_mode", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -199,11 +234,14 @@ func (mp MediaPlayer) SelectSoundMode(entityID string, serviceData map[string]an // Takes an entityID and an optional // map that is translated into service_data. func (mp MediaPlayer) SelectSource(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "select_source" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "media_player", + Service: "select_source", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -215,11 +253,14 @@ func (mp MediaPlayer) SelectSource(entityID string, serviceData map[string]any) // Takes an entityID and an optional // map that is translated into service_data. func (mp MediaPlayer) Shuffle(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "shuffle_set" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "media_player", + Service: "shuffle_set", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -230,10 +271,13 @@ func (mp MediaPlayer) Shuffle(entityID string, serviceData map[string]any) { // Toggles a media player power state. // Takes an entityID. func (mp MediaPlayer) Toggle(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "toggle" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "toggle", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -244,10 +288,13 @@ func (mp MediaPlayer) Toggle(entityID string) { // Turn a media player power off. // Takes an entityID. func (mp MediaPlayer) TurnOff(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "turn_off" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "turn_off", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -258,10 +305,13 @@ func (mp MediaPlayer) TurnOff(entityID string) { // Turn a media player power on. // Takes an entityID. func (mp MediaPlayer) TurnOn(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "turn_on" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "turn_on", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -273,10 +323,13 @@ func (mp MediaPlayer) TurnOn(entityID string) { // platforms with support for player groups. // Takes an entityID. func (mp MediaPlayer) Unjoin(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "unjoin" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "unjoin", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -287,10 +340,13 @@ func (mp MediaPlayer) Unjoin(entityID string) { // Turn a media player volume down. // Takes an entityID. func (mp MediaPlayer) VolumeDown(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "volume_down" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "volume_down", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -302,11 +358,14 @@ func (mp MediaPlayer) VolumeDown(entityID string) { // Takes an entityID and an optional // map that is translated into service_data. func (mp MediaPlayer) VolumeMute(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "volume_mute" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "media_player", + Service: "volume_mute", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -318,11 +377,14 @@ func (mp MediaPlayer) VolumeMute(entityID string, serviceData map[string]any) { // Takes an entityID and an optional // map that is translated into service_data. func (mp MediaPlayer) VolumeSet(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "volume_set" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "media_player", + Service: "volume_set", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -333,10 +395,13 @@ func (mp MediaPlayer) VolumeSet(entityID string, serviceData map[string]any) { // Turn a media player volume up. // Takes an entityID. func (mp MediaPlayer) VolumeUp(entityID string) { - req := CallServiceRequest{} - req.Domain = "media_player" - req.Service = "volume_up" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "media_player", + Service: "volume_up", + Target: Target{ + EntityID: entityID, + }, + } mp.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/notify.go b/internal/services/notify.go index 9b0bcc2..b6b420b 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -15,20 +15,21 @@ func NewNotify(conn *websocket.Conn) *Notify { } } -// Send a notification. Takes a types.NotifyRequest. +// Send a notification. func (ha *Notify) Notify(reqData types.NotifyRequest) { - req := CallServiceRequest{} - req.Domain = "notify" - req.Service = reqData.ServiceName - - serviceData := map[string]any{} - serviceData["message"] = reqData.Message - serviceData["title"] = reqData.Title + serviceData := map[string]any{ + "message": reqData.Message, + "title": reqData.Title, + } if reqData.Data != nil { serviceData["data"] = reqData.Data } - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "notify", + Service: reqData.ServiceName, + ServiceData: serviceData, + } ha.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/number.go b/internal/services/number.go index c3be8b8..656677c 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -19,11 +19,14 @@ func NewNumber(conn *websocket.Conn) *Number { /* Public API */ func (ib Number) SetValue(entityID string, value float32) { - req := CallServiceRequest{} - req.Domain = "number" - req.Service = "set_value" - req.Target.EntityID = entityID - req.ServiceData = map[string]any{"value": value} + req := CallServiceRequest{ + Domain: "number", + Service: "set_value", + Target: Target{ + EntityID: entityID, + }, + ServiceData: map[string]any{"value": value}, + } ib.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/scene.go b/internal/services/scene.go index 73bd8e3..54df52f 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -20,10 +20,11 @@ func NewScene(conn *websocket.Conn) *Scene { // Apply a scene. Takes map that is translated into service_data. func (s Scene) Apply(serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "scene" - req.Service = "apply" - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "scene", + Service: "apply", + ServiceData: serviceData, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -33,11 +34,14 @@ func (s Scene) Apply(serviceData map[string]any) { // Create a scene entity. func (s Scene) Create(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "scene" - req.Service = "create" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "scene", + Service: "create", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -47,9 +51,10 @@ func (s Scene) Create(entityID string, serviceData map[string]any) { // Reload the scenes. func (s Scene) Reload() { - req := CallServiceRequest{} - req.Domain = "scene" - req.Service = "reload" + req := CallServiceRequest{ + Domain: "scene", + Service: "reload", + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -59,11 +64,14 @@ func (s Scene) Reload() { // TurnOn a scene entity. func (s Scene) TurnOn(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "scene" - req.Service = "turn_on" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "scene", + Service: "turn_on", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/script.go b/internal/services/script.go index b017462..7f29a9a 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -20,10 +20,13 @@ func NewScript(conn *websocket.Conn) *Script { // Reload a script that was created in the HA UI. func (s Script) Reload(entityID string) { - req := CallServiceRequest{} - req.Domain = "script" - req.Service = "reload" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "script", + Service: "reload", + Target: Target{ + EntityID: entityID, + }, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -33,10 +36,13 @@ func (s Script) Reload(entityID string) { // Toggle a script that was created in the HA UI. func (s Script) Toggle(entityID string) { - req := CallServiceRequest{} - req.Domain = "script" - req.Service = "toggle" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "script", + Service: "toggle", + Target: Target{ + EntityID: entityID, + }, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -46,9 +52,10 @@ func (s Script) Toggle(entityID string) { // Turn off a script that was created in the HA UI. func (s Script) TurnOff() { - req := CallServiceRequest{} - req.Domain = "script" - req.Service = "turn_off" + req := CallServiceRequest{ + Domain: "script", + Service: "turn_off", + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -58,10 +65,13 @@ func (s Script) TurnOff() { // Turn on a script that was created in the HA UI. func (s Script) TurnOn(entityID string) { - req := CallServiceRequest{} - req.Domain = "script" - req.Service = "turn_on" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "script", + Service: "turn_on", + Target: Target{ + EntityID: entityID, + }, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/services.go b/internal/services/services.go index e5ae32d..009748a 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -11,13 +11,16 @@ func (CallService) MarshalJSON() ([]byte, error) { return []byte(`"call_service"`), nil } +// Target represents the target of the service call, if applicable. +type Target struct { + EntityID string `json:"entity_id,omitempty"` +} + type CallServiceRequest struct { ID int64 `json:"id"` RequestType CallService `json:"type"` // hardcoded "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"` } diff --git a/internal/services/switch.go b/internal/services/switch.go index 46ca452..9a0ac91 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -19,10 +19,13 @@ func NewSwitch(conn *websocket.Conn) *Switch { /* Public API */ func (s Switch) TurnOn(entityID string) { - req := CallServiceRequest{} - req.Domain = "switch" - req.Service = "turn_on" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "switch", + Service: "turn_on", + Target: Target{ + EntityID: entityID, + }, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -31,10 +34,13 @@ func (s Switch) TurnOn(entityID string) { } func (s Switch) Toggle(entityID string) { - req := CallServiceRequest{} - req.Domain = "switch" - req.Service = "toggle" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "switch", + Service: "toggle", + Target: Target{ + EntityID: entityID, + }, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -43,10 +49,13 @@ func (s Switch) Toggle(entityID string) { } func (s Switch) TurnOff(entityID string) { - req := CallServiceRequest{} - req.Domain = "switch" - req.Service = "turn_off" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "switch", + Service: "turn_off", + Target: Target{ + EntityID: entityID, + }, + } s.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/tts.go b/internal/services/tts.go index 9f0a8be..a139db7 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -20,9 +20,10 @@ func NewTTS(conn *websocket.Conn) *TTS { // Remove all text-to-speech cache files and RAM cache. func (tts TTS) ClearCache() { - req := CallServiceRequest{} - req.Domain = "tts" - req.Service = "clear_cache" + req := CallServiceRequest{ + Domain: "tts", + Service: "clear_cache", + } tts.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -32,11 +33,14 @@ func (tts TTS) ClearCache() { // Say something using text-to-speech on a media player with cloud. func (tts TTS) CloudSay(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "tts" - req.Service = "cloud_say" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "tts", + Service: "cloud_say", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } tts.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -47,11 +51,14 @@ func (tts TTS) CloudSay(entityID string, serviceData map[string]any) { // Say something using text-to-speech on a media player with // google_translate. func (tts TTS) GoogleTranslateSay(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "tts" - req.Service = "google_translate_say" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "tts", + Service: "google_translate_say", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } tts.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 5dea65c..e20f0fe 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -20,10 +20,13 @@ func NewVacuum(conn *websocket.Conn) *Vacuum { // Tell the vacuum cleaner to do a spot clean-up. func (v Vacuum) CleanSpot(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "clean_spot" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "clean_spot", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -33,10 +36,13 @@ func (v Vacuum) CleanSpot(entityID string) { // Locate the vacuum cleaner robot. func (v Vacuum) Locate(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "locate" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "locate", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -46,10 +52,13 @@ func (v Vacuum) Locate(entityID string) { // Pause the cleaning task. func (v Vacuum) Pause(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "pause" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "pause", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -59,10 +68,13 @@ func (v Vacuum) Pause(entityID string) { // Tell the vacuum cleaner to return to its dock. func (v Vacuum) ReturnToBase(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "return_to_base" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "return_to_base", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -72,11 +84,14 @@ func (v Vacuum) ReturnToBase(entityID string) { // Send a raw command to the vacuum cleaner. func (v Vacuum) SendCommand(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "send_command" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "vacuum", + Service: "send_command", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -86,11 +101,14 @@ func (v Vacuum) SendCommand(entityID string, serviceData map[string]any) { // Set the fan speed of the vacuum cleaner. func (v Vacuum) SetFanSpeed(entityID string, serviceData map[string]any) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "set_fan_speed" - req.Target.EntityID = entityID - req.ServiceData = serviceData + req := CallServiceRequest{ + Domain: "vacuum", + Service: "set_fan_speed", + Target: Target{ + EntityID: entityID, + }, + ServiceData: serviceData, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -100,10 +118,13 @@ func (v Vacuum) SetFanSpeed(entityID string, serviceData map[string]any) { // Start or resume the cleaning task. func (v Vacuum) Start(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "start" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "start", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -113,10 +134,13 @@ func (v Vacuum) Start(entityID string) { // Start, pause, or resume the cleaning task. func (v Vacuum) StartPause(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "start_pause" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "start_pause", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -126,10 +150,13 @@ func (v Vacuum) StartPause(entityID string) { // Stop the current cleaning task. func (v Vacuum) Stop(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "stop" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "stop", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -139,10 +166,13 @@ func (v Vacuum) Stop(entityID string) { // Stop the current cleaning task and return to home. func (v Vacuum) TurnOff(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "turn_off" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "turn_off", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() @@ -152,10 +182,13 @@ func (v Vacuum) TurnOff(entityID string) { // Start a new cleaning task. func (v Vacuum) TurnOn(entityID string) { - req := CallServiceRequest{} - req.Domain = "vacuum" - req.Service = "turn_on" - req.Target.EntityID = entityID + req := CallServiceRequest{ + Domain: "vacuum", + Service: "turn_on", + Target: Target{ + EntityID: entityID, + }, + } v.conn.Send(func(mw websocket.MessageWriter) error { req.ID = mw.NextID() diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 7bcbf7a..e9711a1 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -20,13 +20,16 @@ func NewZWaveJS(conn *websocket.Conn) *ZWaveJS { // ZWaveJS bulk_set_partial_config_parameters service. func (zw ZWaveJS) BulkSetPartialConfigParam(entityID string, parameter int, value any) { - req := CallServiceRequest{} - req.Domain = "zwave_js" - req.Service = "bulk_set_partial_config_parameters" - req.Target.EntityID = entityID - req.ServiceData = map[string]any{ - "parameter": parameter, - "value": value, + req := CallServiceRequest{ + Domain: "zwave_js", + Service: "bulk_set_partial_config_parameters", + Target: Target{ + EntityID: entityID, + }, + ServiceData: map[string]any{ + "parameter": parameter, + "value": value, + }, } zw.conn.Send(func(mw websocket.MessageWriter) error { From 8e146c178c3d27b73be20b957f1851479d95b936 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 20 Apr 2024 12:57:26 +0200 Subject: [PATCH 041/103] Remove `types` package Move type definitions closer to where they are used. --- internal/services/climate.go | 29 +++++++++++++++++++++++++---- internal/services/notify.go | 11 +++++++++-- types/requestTypes.go | 33 --------------------------------- 3 files changed, 34 insertions(+), 39 deletions(-) delete mode 100644 types/requestTypes.go diff --git a/internal/services/climate.go b/internal/services/climate.go index d5354dd..b3beadc 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -2,7 +2,6 @@ package services import ( "saml.dev/gome-assistant/internal/websocket" - "saml.dev/gome-assistant/types" ) /* Structs */ @@ -17,8 +16,6 @@ func NewClimate(conn *websocket.Conn) *Climate { } } -/* Public API */ - func (c Climate) SetFanMode(entityID string, fanMode string) { req := CallServiceRequest{ Domain: "climate", @@ -35,7 +32,31 @@ func (c Climate) SetFanMode(entityID string, fanMode string) { }) } -func (c Climate) SetTemperature(entityID string, serviceData types.SetTemperatureRequest) { +type SetTemperatureRequest struct { + Temperature float32 + TargetTempHigh float32 + TargetTempLow float32 + HvacMode string +} + +func (r *SetTemperatureRequest) ToJSON() map[string]any { + m := map[string]any{} + if r.Temperature != 0 { + m["temperature"] = r.Temperature + } + if r.TargetTempHigh != 0 { + m["target_temp_high"] = r.TargetTempHigh + } + if r.TargetTempLow != 0 { + m["target_temp_low"] = r.TargetTempLow + } + if r.HvacMode != "" { + m["hvac_mode"] = r.HvacMode + } + return m +} + +func (c Climate) SetTemperature(entityID string, serviceData SetTemperatureRequest) { req := CallServiceRequest{ Domain: "climate", Service: "set_temperature", diff --git a/internal/services/notify.go b/internal/services/notify.go index b6b420b..e27bc94 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -2,7 +2,6 @@ package services import ( "saml.dev/gome-assistant/internal/websocket" - "saml.dev/gome-assistant/types" ) type Notify struct { @@ -15,8 +14,16 @@ func NewNotify(conn *websocket.Conn) *Notify { } } +type NotifyRequest struct { + // Which notify service to call, such as mobile_app_sams_iphone + ServiceName string + Message string + Title string + Data map[string]any +} + // Send a notification. -func (ha *Notify) Notify(reqData types.NotifyRequest) { +func (ha *Notify) Notify(reqData NotifyRequest) { serviceData := map[string]any{ "message": reqData.Message, "title": reqData.Title, diff --git a/types/requestTypes.go b/types/requestTypes.go deleted file mode 100644 index a2f736f..0000000 --- a/types/requestTypes.go +++ /dev/null @@ -1,33 +0,0 @@ -package types - -type NotifyRequest struct { - // Which notify service to call, such as mobile_app_sams_iphone - ServiceName string - Message string - Title string - Data map[string]any -} - -type SetTemperatureRequest struct { - Temperature float32 - TargetTempHigh float32 - TargetTempLow float32 - HvacMode string -} - -func (r *SetTemperatureRequest) ToJSON() map[string]any { - m := map[string]any{} - if r.Temperature != 0 { - m["temperature"] = r.Temperature - } - if r.TargetTempHigh != 0 { - m["target_temp_high"] = r.TargetTempHigh - } - if r.TargetTempLow != 0 { - m["target_temp_low"] = r.TargetTempLow - } - if r.HvacMode != "" { - m["hvac_mode"] = r.HvacMode - } - return m -} From 448500953152cea1b90bb8f57adfcad6b7b4378b Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 20 Apr 2024 13:14:00 +0200 Subject: [PATCH 042/103] Change `serviceData` types from `map[string]any` to `any` This gives the callers more freedom in setting them. --- internal/services/alarm_control_panel.go | 14 +++++++------- internal/services/cover.go | 4 ++-- internal/services/homeassistant.go | 4 ++-- internal/services/light.go | 4 ++-- internal/services/lock.go | 4 ++-- internal/services/media_player.go | 18 +++++++++--------- internal/services/scene.go | 6 +++--- internal/services/services.go | 15 +++++++++------ internal/services/tts.go | 4 ++-- internal/services/vacuum.go | 4 ++-- 10 files changed, 40 insertions(+), 37 deletions(-) diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 7eed9eb..e4a776b 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -19,7 +19,7 @@ func NewAlarmControlPanel(conn *websocket.Conn) *AlarmControlPanel { } // Send the alarm the command for arm away. -func (acp AlarmControlPanel) ArmAway(entityID string, serviceData map[string]any) { +func (acp AlarmControlPanel) ArmAway(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "alarm_control_panel", Service: "alarm_arm_away", @@ -38,7 +38,7 @@ func (acp AlarmControlPanel) ArmAway(entityID string, serviceData map[string]any // 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) { +func (acp AlarmControlPanel) ArmWithCustomBypass(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "alarm_control_panel", Service: "alarm_arm_custom_bypass", @@ -57,7 +57,7 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityID string, serviceData ma // 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) { +func (acp AlarmControlPanel) ArmHome(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "alarm_control_panel", Service: "alarm_arm_home", @@ -74,7 +74,7 @@ func (acp AlarmControlPanel) ArmHome(entityID string, serviceData map[string]any } // Send the alarm the command for arm night. -func (acp AlarmControlPanel) ArmNight(entityID string, serviceData map[string]any) { +func (acp AlarmControlPanel) ArmNight(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "alarm_control_panel", Service: "alarm_arm_night", @@ -91,7 +91,7 @@ func (acp AlarmControlPanel) ArmNight(entityID string, serviceData map[string]an } // Send the alarm the command for arm vacation. -func (acp AlarmControlPanel) ArmVacation(entityID string, serviceData map[string]any) { +func (acp AlarmControlPanel) ArmVacation(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "alarm_control_panel", Service: "alarm_arm_vacation", @@ -108,7 +108,7 @@ func (acp AlarmControlPanel) ArmVacation(entityID string, serviceData map[string } // Send the alarm the command for disarm. -func (acp AlarmControlPanel) Disarm(entityID string, serviceData map[string]any) { +func (acp AlarmControlPanel) Disarm(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "alarm_control_panel", Service: "alarm_disarm", @@ -125,7 +125,7 @@ func (acp AlarmControlPanel) Disarm(entityID string, serviceData map[string]any) } // Send the alarm the command for trigger. -func (acp AlarmControlPanel) Trigger(entityID string, serviceData map[string]any) { +func (acp AlarmControlPanel) Trigger(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "alarm_control_panel", Service: "alarm_trigger", diff --git a/internal/services/cover.go b/internal/services/cover.go index 7437b17..a9cdf51 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -84,7 +84,7 @@ func (c Cover) OpenTilt(entityID string) { // 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) { +func (c Cover) SetPosition(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "cover", Service: "set_cover_position", @@ -102,7 +102,7 @@ func (c Cover) SetPosition(entityID string, serviceData map[string]any) { // 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) { +func (c Cover) SetTiltPosition(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "cover", Service: "set_cover_tilt_position", diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index aac4aaa..593d36a 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -16,7 +16,7 @@ func NewHomeAssistant(conn *websocket.Conn) *HomeAssistant { // 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) { +func (ha *HomeAssistant) TurnOn(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "homeassistant", Service: "turn_on", @@ -34,7 +34,7 @@ func (ha *HomeAssistant) TurnOn(entityID string, serviceData map[string]any) { // 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) { +func (ha *HomeAssistant) Toggle(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "homeassistant", Service: "toggle", diff --git a/internal/services/light.go b/internal/services/light.go index bf54848..e9ec1bd 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -19,7 +19,7 @@ func NewLight(conn *websocket.Conn) *Light { /* Public API */ // TurnOn a light entity. -func (l Light) TurnOn(entityID string, serviceData map[string]any) { +func (l Light) TurnOn(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "light", Service: "turn_on", @@ -36,7 +36,7 @@ func (l Light) TurnOn(entityID string, serviceData map[string]any) { } // Toggle a light entity. -func (l Light) Toggle(entityID string, serviceData map[string]any) { +func (l Light) Toggle(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "light", Service: "toggle", diff --git a/internal/services/lock.go b/internal/services/lock.go index c3b665b..51d4d03 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -19,7 +19,7 @@ func NewLock(conn *websocket.Conn) *Lock { /* Public API */ // Lock a lock entity. -func (l Lock) Lock(entityID string, serviceData map[string]any) { +func (l Lock) Lock(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "lock", Service: "lock", @@ -36,7 +36,7 @@ func (l Lock) Lock(entityID string, serviceData map[string]any) { } // Unlock a lock entity. -func (l Lock) Unlock(entityID string, serviceData map[string]any) { +func (l Lock) Unlock(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "lock", Service: "unlock", diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 664ac3b..486b4d3 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -38,7 +38,7 @@ func (mp MediaPlayer) ClearPlaylist(entityID string) { // 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) { +func (mp MediaPlayer) Join(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "join", @@ -142,7 +142,7 @@ func (mp MediaPlayer) Previous(entityID string) { // 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) { +func (mp MediaPlayer) Seek(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "media_seek", @@ -177,7 +177,7 @@ func (mp MediaPlayer) Stop(entityID string) { // 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) { +func (mp MediaPlayer) PlayMedia(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "play_media", @@ -195,7 +195,7 @@ func (mp MediaPlayer) PlayMedia(entityID string, serviceData map[string]any) { // 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) { +func (mp MediaPlayer) RepeatSet(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "repeat_set", @@ -214,7 +214,7 @@ func (mp MediaPlayer) RepeatSet(entityID string, serviceData map[string]any) { // 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) { +func (mp MediaPlayer) SelectSoundMode(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "select_sound_mode", @@ -233,7 +233,7 @@ func (mp MediaPlayer) SelectSoundMode(entityID string, serviceData map[string]an // 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) { +func (mp MediaPlayer) SelectSource(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "select_source", @@ -252,7 +252,7 @@ func (mp MediaPlayer) SelectSource(entityID string, serviceData map[string]any) // 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) { +func (mp MediaPlayer) Shuffle(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "shuffle_set", @@ -357,7 +357,7 @@ func (mp MediaPlayer) VolumeDown(entityID string) { // 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) { +func (mp MediaPlayer) VolumeMute(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "volume_mute", @@ -376,7 +376,7 @@ func (mp MediaPlayer) VolumeMute(entityID string, serviceData map[string]any) { // 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) { +func (mp MediaPlayer) VolumeSet(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "media_player", Service: "volume_set", diff --git a/internal/services/scene.go b/internal/services/scene.go index 54df52f..ea9c50a 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -19,7 +19,7 @@ func NewScene(conn *websocket.Conn) *Scene { /* Public API */ // Apply a scene. Takes map that is translated into service_data. -func (s Scene) Apply(serviceData map[string]any) { +func (s Scene) Apply(serviceData any) { req := CallServiceRequest{ Domain: "scene", Service: "apply", @@ -33,7 +33,7 @@ func (s Scene) Apply(serviceData map[string]any) { } // Create a scene entity. -func (s Scene) Create(entityID string, serviceData map[string]any) { +func (s Scene) Create(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "scene", Service: "create", @@ -63,7 +63,7 @@ func (s Scene) Reload() { } // TurnOn a scene entity. -func (s Scene) TurnOn(entityID string, serviceData map[string]any) { +func (s Scene) TurnOn(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "scene", Service: "turn_on", diff --git a/internal/services/services.go b/internal/services/services.go index 009748a..f454734 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -17,10 +17,13 @@ type Target struct { } type CallServiceRequest struct { - ID int64 `json:"id"` - RequestType CallService `json:"type"` // hardcoded "call_service" - Domain string `json:"domain"` - Service string `json:"service"` - ServiceData map[string]any `json:"service_data,omitempty"` - Target Target `json:"target,omitempty"` + ID int64 `json:"id"` + RequestType CallService `json:"type"` // hardcoded "call_service" + Domain string `json:"domain"` + Service string `json:"service"` + + // ServiceData must be serializable to a JSON object. + ServiceData any `json:"service_data,omitempty"` + + Target Target `json:"target,omitempty"` } diff --git a/internal/services/tts.go b/internal/services/tts.go index a139db7..98fed8a 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -32,7 +32,7 @@ func (tts TTS) ClearCache() { } // Say something using text-to-speech on a media player with cloud. -func (tts TTS) CloudSay(entityID string, serviceData map[string]any) { +func (tts TTS) CloudSay(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "tts", Service: "cloud_say", @@ -50,7 +50,7 @@ func (tts TTS) CloudSay(entityID string, serviceData map[string]any) { // Say something using text-to-speech on a media player with // google_translate. -func (tts TTS) GoogleTranslateSay(entityID string, serviceData map[string]any) { +func (tts TTS) GoogleTranslateSay(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "tts", Service: "google_translate_say", diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index e20f0fe..90288f1 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -83,7 +83,7 @@ func (v Vacuum) ReturnToBase(entityID string) { } // Send a raw command to the vacuum cleaner. -func (v Vacuum) SendCommand(entityID string, serviceData map[string]any) { +func (v Vacuum) SendCommand(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "vacuum", Service: "send_command", @@ -100,7 +100,7 @@ func (v Vacuum) SendCommand(entityID string, serviceData map[string]any) { } // Set the fan speed of the vacuum cleaner. -func (v Vacuum) SetFanSpeed(entityID string, serviceData map[string]any) { +func (v Vacuum) SetFanSpeed(entityID string, serviceData any) { req := CallServiceRequest{ Domain: "vacuum", Service: "set_fan_speed", From 06927b25ffc3ace8276f5f101585407b30332884 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 20 Apr 2024 13:27:05 +0200 Subject: [PATCH 043/103] =?UTF-8?q?SetTemperatureRequest:=20distinguish=20?= =?UTF-8?q?between=200=C2=B0C=20and=20"unset"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/services/climate.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/services/climate.go b/internal/services/climate.go index b3beadc..03fa0e9 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -33,22 +33,22 @@ func (c Climate) SetFanMode(entityID string, fanMode string) { } type SetTemperatureRequest struct { - Temperature float32 - TargetTempHigh float32 - TargetTempLow float32 + Temperature *float32 + TargetTempHigh *float32 + TargetTempLow *float32 HvacMode string } func (r *SetTemperatureRequest) ToJSON() map[string]any { m := map[string]any{} - if r.Temperature != 0 { - m["temperature"] = r.Temperature + if r.Temperature != nil { + m["temperature"] = *r.Temperature } - if r.TargetTempHigh != 0 { - m["target_temp_high"] = r.TargetTempHigh + if r.TargetTempHigh != nil { + m["target_temp_high"] = *r.TargetTempHigh } - if r.TargetTempLow != 0 { - m["target_temp_low"] = r.TargetTempLow + if r.TargetTempLow != nil { + m["target_temp_low"] = *r.TargetTempLow } if r.HvacMode != "" { m["hvac_mode"] = r.HvacMode @@ -56,14 +56,14 @@ func (r *SetTemperatureRequest) ToJSON() map[string]any { return m } -func (c Climate) SetTemperature(entityID string, serviceData SetTemperatureRequest) { +func (c Climate) SetTemperature(entityID string, setTemperatureRequest SetTemperatureRequest) { req := CallServiceRequest{ Domain: "climate", Service: "set_temperature", Target: Target{ EntityID: entityID, }, - ServiceData: serviceData.ToJSON(), + ServiceData: setTemperatureRequest.ToJSON(), } c.conn.Send(func(mw websocket.MessageWriter) error { From 8a89b445724178d74d7070a5edbf5815d57b44d9 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 17:10:22 +0200 Subject: [PATCH 044/103] ChanMsg: remove unused field `Success` --- internal/websocket/read.go | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/internal/websocket/read.go b/internal/websocket/read.go index b2463bd..b56e262 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -13,10 +13,9 @@ type BaseMessage struct { } type ChanMsg struct { - Type string - ID int64 - Success bool - Raw []byte + Type string + ID int64 + Raw []byte } // unsubscribe unsubscribes from `subscription`. It must be called @@ -123,10 +122,9 @@ func (conn *Conn) Start() { slog.Warn("Received unsuccessful response", "response", string(bytes)) } chanMsg := ChanMsg{ - Type: base.Type, - ID: base.ID, - Success: base.Success, - Raw: bytes, + Type: base.Type, + ID: base.ID, + Raw: bytes, } if subscriber, ok := conn.getSubscriber(chanMsg.ID); ok { From 4a7d8fe4fc2d15d09e00427d72c45733e8602469 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 17:15:53 +0200 Subject: [PATCH 045/103] ChanMsg.Raw: change type to `json.RawMessage` --- internal/websocket/read.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/websocket/read.go b/internal/websocket/read.go index b56e262..e226c7a 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -15,7 +15,10 @@ type BaseMessage struct { type ChanMsg struct { Type string ID int64 - Raw []byte + + // Raw contains the original, full, unparsed message (including + // `Type` and `ID`). + Raw json.RawMessage } // unsubscribe unsubscribes from `subscription`. It must be called From be0e941031696ee3a77a88cf89abd4eabc740ca4 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 17:19:05 +0200 Subject: [PATCH 046/103] BaseResultMessage: type renamed from `BaseMessage` --- internal/websocket/read.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/websocket/read.go b/internal/websocket/read.go index e226c7a..3af2620 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -6,7 +6,7 @@ import ( "log/slog" ) -type BaseMessage struct { +type BaseResultMessage struct { Type string `json:"type"` ID int64 `json:"id"` Success bool `json:"success"` @@ -116,7 +116,7 @@ func (conn *Conn) Start() { return } - base := BaseMessage{ + base := BaseResultMessage{ // default to true for messages that don't include "success" at all Success: true, } From 1989fa5527945940c220f8bd6785b77f4dbcb2cb Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 17:23:25 +0200 Subject: [PATCH 047/103] BaseMessage: new type, implementing the minimum fields for a message Embed this type in the other message types. --- entitylistener.go | 3 +-- internal/websocket/read.go | 38 ++++++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/entitylistener.go b/entitylistener.go index 7e1eb3e..d07e301 100644 --- a/entitylistener.go +++ b/entitylistener.go @@ -46,8 +46,7 @@ type EntityData struct { } type stateChangedMsg struct { - ID int `json:"id"` - Type string `json:"type"` + websocket.BaseMessage Event struct { Data struct { EntityID string `json:"entity_id"` diff --git a/internal/websocket/read.go b/internal/websocket/read.go index 3af2620..bc7053a 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -6,18 +6,23 @@ import ( "log/slog" ) +// BaseMessage implements the required part of any websocket message. +// The idea is to embed this type in other message types. +type BaseMessage struct { + Type string `json:"type"` + ID int64 `json:"id"` +} + type BaseResultMessage struct { - Type string `json:"type"` - ID int64 `json:"id"` - Success bool `json:"success"` + BaseMessage + Success bool `json:"success"` } type ChanMsg struct { - Type string - ID int64 + BaseMessage // Raw contains the original, full, unparsed message (including - // `Type` and `ID`). + // fields `Type` and `ID`, which appear in `BaseMessage`). Raw json.RawMessage } @@ -41,8 +46,7 @@ func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { } type SubEvent struct { - ID int64 `json:"id"` - Type string `json:"type"` + BaseMessage EventType string `json:"event_type"` } @@ -53,7 +57,9 @@ type SubEvent struct { func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscription, error) { // Make sure we're listening before events might start arriving: e := SubEvent{ - Type: "subscribe_events", + BaseMessage: BaseMessage{ + Type: "subscribe_events", + }, EventType: eventType, } var subscription Subscription @@ -75,16 +81,17 @@ func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscrip } type UnsubEvent struct { - ID int64 `json:"id"` - Type string `json:"type"` - Subscription int64 `json:"subscription"` + BaseMessage + Subscription int64 `json:"subscription"` } // unwatchEvents unsubscribes to events with the given `subscriptionID`. This does // not remove the subscriber. func (conn *Conn) unwatchEvents(subscriptionID int64) error { e := UnsubEvent{ - Type: "unsubscribe_events", + BaseMessage: BaseMessage{ + Type: "unsubscribe_events", + }, Subscription: subscriptionID, } @@ -125,9 +132,8 @@ func (conn *Conn) Start() { slog.Warn("Received unsuccessful response", "response", string(bytes)) } chanMsg := ChanMsg{ - Type: base.Type, - ID: base.ID, - Raw: bytes, + BaseMessage: base.BaseMessage, + Raw: bytes, } if subscriber, ok := conn.getSubscriber(chanMsg.ID); ok { From e88d4eb7b83907fd6139c062f9051ef9a8dfb099 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 17:28:42 +0200 Subject: [PATCH 048/103] websocket.Message: type renamed from `ChanMsg` --- app.go | 4 ++-- entitylistener.go | 2 +- eventListener.go | 2 +- internal/websocket/read.go | 10 ++++++---- internal/websocket/websocket.go | 2 +- 5 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app.go b/app.go index 8f6334e..079d0c4 100644 --- a/app.go +++ b/app.go @@ -242,7 +242,7 @@ func (app *App) RegisterEventListeners(evls ...EventListener) { // be unsubscribed from. _, err := app.wsConn.WatchEvents( eventType, - func(msg websocket.ChanMsg) { + func(msg websocket.Message) { go app.callEventListeners(msg) }, ) @@ -326,7 +326,7 @@ func (app *App) Start(ctx context.Context) error { // subscribe to state_changed events stateChangedSubscription, err := app.wsConn.WatchStateChangedEvents( - func(msg websocket.ChanMsg) { + func(msg websocket.Message) { go app.callEntityListeners(msg) }, ) diff --git a/entitylistener.go b/entitylistener.go index d07e301..6ecee6c 100644 --- a/entitylistener.go +++ b/entitylistener.go @@ -201,7 +201,7 @@ func (b elBuilder3) Build() EntityListener { } /* Functions */ -func (app *App) callEntityListeners(chanMsg websocket.ChanMsg) { +func (app *App) callEntityListeners(chanMsg websocket.Message) { msgBytes := chanMsg.Raw msg := stateChangedMsg{} json.Unmarshal(msgBytes, &msg) diff --git a/eventListener.go b/eventListener.go index 4ced5ab..8c1ccbc 100644 --- a/eventListener.go +++ b/eventListener.go @@ -159,7 +159,7 @@ type BaseEventMsg struct { } /* Functions */ -func (app *App) callEventListeners(msg websocket.ChanMsg) { +func (app *App) callEventListeners(msg websocket.Message) { baseEventMsg := BaseEventMsg{} json.Unmarshal(msg.Raw, &baseEventMsg) listeners, ok := app.eventListeners[baseEventMsg.Event.EventType] diff --git a/internal/websocket/read.go b/internal/websocket/read.go index bc7053a..84ba37e 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -18,7 +18,9 @@ type BaseResultMessage struct { Success bool `json:"success"` } -type ChanMsg struct { +// Message holds a complete message, only partly parsed. The entire, +// original, unparsed message is available in the `Raw` field. +type Message struct { BaseMessage // Raw contains the original, full, unparsed message (including @@ -131,13 +133,13 @@ func (conn *Conn) Start() { if !base.Success { slog.Warn("Received unsuccessful response", "response", string(bytes)) } - chanMsg := ChanMsg{ + msg := Message{ BaseMessage: base.BaseMessage, Raw: bytes, } - if subscriber, ok := conn.getSubscriber(chanMsg.ID); ok { - subscriber(chanMsg) + if subscriber, ok := conn.getSubscriber(msg.ID); ok { + subscriber(msg) } } } diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index cfb51b4..0fdfa05 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -36,7 +36,7 @@ type Conn struct { // Subscriber is called synchronously when a message with the // subscribed `id` is received. -type Subscriber func(msg ChanMsg) +type Subscriber func(msg Message) type Subscription struct { conn *Conn From da3c113feaa786852a10bb6bbefb4d83851676ef Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 17:37:13 +0200 Subject: [PATCH 049/103] Move the message types to a separate file --- internal/websocket/message.go | 37 +++++++++++++++++++++++++++++++++++ internal/websocket/read.go | 32 ------------------------------ 2 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 internal/websocket/message.go diff --git a/internal/websocket/message.go b/internal/websocket/message.go new file mode 100644 index 0000000..c9ff8e4 --- /dev/null +++ b/internal/websocket/message.go @@ -0,0 +1,37 @@ +package websocket + +import ( + "encoding/json" +) + +// BaseMessage implements the required part of any websocket message. +// The idea is to embed this type in other message types. +type BaseMessage struct { + Type string `json:"type"` + ID int64 `json:"id"` +} + +type BaseResultMessage struct { + BaseMessage + Success bool `json:"success"` +} + +// Message holds a complete message, only partly parsed. The entire, +// original, unparsed message is available in the `Raw` field. +type Message struct { + BaseMessage + + // Raw contains the original, full, unparsed message (including + // fields `Type` and `ID`, which appear in `BaseMessage`). + Raw json.RawMessage +} + +type SubEvent struct { + BaseMessage + EventType string `json:"event_type"` +} + +type UnsubEvent struct { + BaseMessage + Subscription int64 `json:"subscription"` +} diff --git a/internal/websocket/read.go b/internal/websocket/read.go index 84ba37e..fb8c636 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -6,28 +6,6 @@ import ( "log/slog" ) -// BaseMessage implements the required part of any websocket message. -// The idea is to embed this type in other message types. -type BaseMessage struct { - Type string `json:"type"` - ID int64 `json:"id"` -} - -type BaseResultMessage struct { - BaseMessage - Success bool `json:"success"` -} - -// Message holds a complete message, only partly parsed. The entire, -// original, unparsed message is available in the `Raw` field. -type Message struct { - BaseMessage - - // Raw contains the original, full, unparsed message (including - // fields `Type` and `ID`, which appear in `BaseMessage`). - Raw json.RawMessage -} - // unsubscribe unsubscribes from `subscription`. It must be called // exactly once for each subscription. It must be invoked while // holding the `subscribeMutex` for writing. @@ -47,11 +25,6 @@ func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { return subscriber, ok } -type SubEvent struct { - BaseMessage - EventType string `json:"event_type"` -} - // WatchEvents subscribes to events of the given type, invoking // `subscriber` when any such events are received. Calls to // `subscriber` are synchronous with respect to any other received @@ -82,11 +55,6 @@ func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscrip return subscription, nil } -type UnsubEvent struct { - BaseMessage - Subscription int64 `json:"subscription"` -} - // unwatchEvents unsubscribes to events with the given `subscriptionID`. This does // not remove the subscriber. func (conn *Conn) unwatchEvents(subscriptionID int64) error { From 9ca032d8af0eef5cb3d9f6dca91dc3871625fd09 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 17:42:54 +0200 Subject: [PATCH 050/103] authRequest: rename type from `AuthMessage` This shouldn't be public. --- internal/websocket/websocket.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 0fdfa05..f097148 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -17,11 +17,6 @@ import ( var ErrInvalidToken = errors.New("invalid authentication token") -type AuthMessage struct { - MsgType string `json:"type"` - AccessToken string `json:"access_token"` -} - type Conn struct { writeMutex sync.Mutex conn *websocket.Conn @@ -124,8 +119,13 @@ func (conn *Conn) Close() error { return conn.conn.Close() } +type authRequest struct { + MsgType string `json:"type"` + AccessToken string `json:"access_token"` +} + func (conn *Conn) sendAuthMessage(token string) error { - err := conn.conn.WriteJSON(AuthMessage{MsgType: "auth", AccessToken: token}) + err := conn.conn.WriteJSON(authRequest{MsgType: "auth", AccessToken: token}) if err != nil { return err } From 5e6aa5d43e94711251a6f9a2fa1c8c7dd2947a8b Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 17:59:26 +0200 Subject: [PATCH 051/103] MessageWriter: add an `Unsubscribe()` method This will become the main way to unsubscribe at the websocket level. --- internal/websocket/send.go | 15 +++++++++++++++ internal/websocket/websocket.go | 3 +++ 2 files changed, 18 insertions(+) diff --git a/internal/websocket/send.go b/internal/websocket/send.go index e582196..6b900ac 100644 --- a/internal/websocket/send.go +++ b/internal/websocket/send.go @@ -5,6 +5,7 @@ import "fmt" type MessageWriter interface { NextID() int64 Subscribe(subscriber Subscriber) Subscription + Unsubscribe(subscription Subscription) SendMessage(msg any) error } @@ -69,3 +70,17 @@ func (mw connMessageWriter) Subscribe(subscriber Subscriber) Subscription { id: id, } } + +// Unsubscribe terminates `subscription` at the websocket level; i.e., +// no more incoming messages will be forwarded to the corresponding +// `Subscriber`. Note that this does not interact with the server; it +// is the caller's responsibility to send it an "unsubscribe" command +// if necessary. +func (mw connMessageWriter) Unsubscribe(subscription Subscription) { + if subscription.id == 0 { + return + } + + subscription.conn.unsubscribe(subscription.id) + subscription.id = 0 +} diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index f097148..4d3e180 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -33,6 +33,9 @@ type Conn struct { // subscribed `id` is received. type Subscriber func(msg Message) +// Subscription represents a websocket-level subscription to a +// particular message ID. Incoming messages with that ID will be +// forwarded to the corresponding `Subscriber`. type Subscription struct { conn *Conn id int64 From 8b6fe9407476b147b0153b681f110667364d1354 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 18:10:11 +0200 Subject: [PATCH 052/103] LockedConn: type renamed from `MessageWriter` Also, move it to its own file and rename `connMessageWriter` to `lockedConn`. --- internal/services/alarm_control_panel.go | 42 ++++---- internal/services/climate.go | 12 +-- internal/services/cover.go | 60 +++++------ internal/services/event.go | 6 +- internal/services/homeassistant.go | 18 ++-- internal/services/input_boolean.go | 24 ++--- internal/services/input_button.go | 12 +-- internal/services/input_datetime.go | 12 +-- internal/services/input_number.go | 24 ++--- internal/services/input_text.go | 12 +-- internal/services/light.go | 18 ++-- internal/services/lock.go | 12 +-- internal/services/media_player.go | 132 +++++++++++------------ internal/services/notify.go | 6 +- internal/services/number.go | 6 +- internal/services/scene.go | 24 ++--- internal/services/script.go | 24 ++--- internal/services/switch.go | 18 ++-- internal/services/tts.go | 18 ++-- internal/services/vacuum.go | 66 ++++++------ internal/services/zwavejs.go | 6 +- internal/websocket/locked_conn.go | 10 ++ internal/websocket/read.go | 12 +-- internal/websocket/send.go | 55 +++++----- 24 files changed, 318 insertions(+), 311 deletions(-) create mode 100644 internal/websocket/locked_conn.go diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index e4a776b..95c5436 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -29,9 +29,9 @@ func (acp AlarmControlPanel) ArmAway(entityID string, serviceData any) { ServiceData: serviceData, } - acp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + acp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -48,9 +48,9 @@ func (acp AlarmControlPanel) ArmWithCustomBypass(entityID string, serviceData an ServiceData: serviceData, } - acp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + acp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -67,9 +67,9 @@ func (acp AlarmControlPanel) ArmHome(entityID string, serviceData any) { ServiceData: serviceData, } - acp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + acp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -84,9 +84,9 @@ func (acp AlarmControlPanel) ArmNight(entityID string, serviceData any) { ServiceData: serviceData, } - acp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + acp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -101,9 +101,9 @@ func (acp AlarmControlPanel) ArmVacation(entityID string, serviceData any) { ServiceData: serviceData, } - acp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + acp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -118,9 +118,9 @@ func (acp AlarmControlPanel) Disarm(entityID string, serviceData any) { ServiceData: serviceData, } - acp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + acp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -135,8 +135,8 @@ func (acp AlarmControlPanel) Trigger(entityID string, serviceData any) { ServiceData: serviceData, } - acp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + acp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/climate.go b/internal/services/climate.go index 03fa0e9..a9d2e35 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -26,9 +26,9 @@ func (c Climate) SetFanMode(entityID string, fanMode string) { ServiceData: map[string]any{"fan_mode": fanMode}, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -66,8 +66,8 @@ func (c Climate) SetTemperature(entityID string, setTemperatureRequest SetTemper ServiceData: setTemperatureRequest.ToJSON(), } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/cover.go b/internal/services/cover.go index a9cdf51..c732966 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -28,9 +28,9 @@ func (c Cover) Close(entityID string) { }, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -44,9 +44,9 @@ func (c Cover) CloseTilt(entityID string) { }, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -60,9 +60,9 @@ func (c Cover) Open(entityID string) { }, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -76,9 +76,9 @@ func (c Cover) OpenTilt(entityID string) { }, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -94,9 +94,9 @@ func (c Cover) SetPosition(entityID string, serviceData any) { ServiceData: serviceData, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -112,9 +112,9 @@ func (c Cover) SetTiltPosition(entityID string, serviceData any) { ServiceData: serviceData, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -128,9 +128,9 @@ func (c Cover) Stop(entityID string) { }, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -144,9 +144,9 @@ func (c Cover) StopTilt(entityID string) { }, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -160,9 +160,9 @@ func (c Cover) Toggle(entityID string) { }, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -176,8 +176,8 @@ func (c Cover) ToggleTilt(entityID string) { }, } - c.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + c.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/event.go b/internal/services/event.go index 44d4bcb..ca212d2 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -34,8 +34,8 @@ func (e Event) Fire(eventType string, eventData map[string]any) { req.EventType = eventType req.EventData = eventData - e.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + e.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 593d36a..9c63428 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -26,9 +26,9 @@ func (ha *HomeAssistant) TurnOn(entityID string, serviceData any) { ServiceData: serviceData, } - ha.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ha.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -44,9 +44,9 @@ func (ha *HomeAssistant) Toggle(entityID string, serviceData any) { ServiceData: serviceData, } - ha.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ha.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -59,8 +59,8 @@ func (ha *HomeAssistant) TurnOff(entityID string) { }, } - ha.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ha.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 662cba5..363b83d 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -27,9 +27,9 @@ func (ib InputBoolean) TurnOn(entityID string) { }, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -42,9 +42,9 @@ func (ib InputBoolean) Toggle(entityID string) { }, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -57,9 +57,9 @@ func (ib InputBoolean) TurnOff(entityID string) { }, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -69,8 +69,8 @@ func (ib InputBoolean) Reload() { Service: "reload", } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index e50d5a0..5bb880f 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -27,9 +27,9 @@ func (ib InputButton) Press(entityID string) { }, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -39,8 +39,8 @@ func (ib InputButton) Reload() { Service: "reload", } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index f609a40..bca19e8 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -33,9 +33,9 @@ func (ib InputDatetime) Set(entityID string, value time.Time) { }, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -45,8 +45,8 @@ func (ib InputDatetime) Reload() { Service: "reload", } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 297eda8..e7c818f 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -28,9 +28,9 @@ func (ib InputNumber) Set(entityID string, value float32) { ServiceData: map[string]any{"value": value}, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -43,9 +43,9 @@ func (ib InputNumber) Increment(entityID string) { }, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -58,9 +58,9 @@ func (ib InputNumber) Decrement(entityID string) { }, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -70,8 +70,8 @@ func (ib InputNumber) Reload() { Service: "reload", } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index ae593b0..45b1e06 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -30,9 +30,9 @@ func (ib InputText) Set(entityID string, value string) { }, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -42,8 +42,8 @@ func (ib InputText) Reload() { Service: "reload", } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/light.go b/internal/services/light.go index e9ec1bd..7134085 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -29,9 +29,9 @@ func (l Light) TurnOn(entityID string, serviceData any) { ServiceData: serviceData, } - l.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + l.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -46,9 +46,9 @@ func (l Light) Toggle(entityID string, serviceData any) { ServiceData: serviceData, } - l.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + l.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -61,8 +61,8 @@ func (l Light) TurnOff(entityID string) { }, } - l.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + l.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/lock.go b/internal/services/lock.go index 51d4d03..1f77dfa 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -29,9 +29,9 @@ func (l Lock) Lock(entityID string, serviceData any) { ServiceData: serviceData, } - l.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + l.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -46,8 +46,8 @@ func (l Lock) Unlock(entityID string, serviceData any) { ServiceData: serviceData, } - l.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + l.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 486b4d3..0b8e263 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -29,9 +29,9 @@ func (mp MediaPlayer) ClearPlaylist(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -48,9 +48,9 @@ func (mp MediaPlayer) Join(entityID string, serviceData any) { ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -65,9 +65,9 @@ func (mp MediaPlayer) Next(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -82,9 +82,9 @@ func (mp MediaPlayer) Pause(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -99,9 +99,9 @@ func (mp MediaPlayer) Play(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -116,9 +116,9 @@ func (mp MediaPlayer) PlayPause(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -133,9 +133,9 @@ func (mp MediaPlayer) Previous(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -151,9 +151,9 @@ func (mp MediaPlayer) Seek(entityID string, serviceData any) { }, ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -168,9 +168,9 @@ func (mp MediaPlayer) Stop(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -187,9 +187,9 @@ func (mp MediaPlayer) PlayMedia(entityID string, serviceData any) { ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -205,9 +205,9 @@ func (mp MediaPlayer) RepeatSet(entityID string, serviceData any) { ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -224,9 +224,9 @@ func (mp MediaPlayer) SelectSoundMode(entityID string, serviceData any) { ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -243,9 +243,9 @@ func (mp MediaPlayer) SelectSource(entityID string, serviceData any) { ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -262,9 +262,9 @@ func (mp MediaPlayer) Shuffle(entityID string, serviceData any) { ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -279,9 +279,9 @@ func (mp MediaPlayer) Toggle(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -296,9 +296,9 @@ func (mp MediaPlayer) TurnOff(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -313,9 +313,9 @@ func (mp MediaPlayer) TurnOn(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -331,9 +331,9 @@ func (mp MediaPlayer) Unjoin(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -348,9 +348,9 @@ func (mp MediaPlayer) VolumeDown(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -367,9 +367,9 @@ func (mp MediaPlayer) VolumeMute(entityID string, serviceData any) { ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -386,9 +386,9 @@ func (mp MediaPlayer) VolumeSet(entityID string, serviceData any) { ServiceData: serviceData, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -403,8 +403,8 @@ func (mp MediaPlayer) VolumeUp(entityID string) { }, } - mp.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + mp.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/notify.go b/internal/services/notify.go index e27bc94..88dbb2a 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -38,8 +38,8 @@ func (ha *Notify) Notify(reqData NotifyRequest) { ServiceData: serviceData, } - ha.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ha.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/number.go b/internal/services/number.go index 656677c..7ccf458 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -28,8 +28,8 @@ func (ib Number) SetValue(entityID string, value float32) { ServiceData: map[string]any{"value": value}, } - ib.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + ib.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/scene.go b/internal/services/scene.go index ea9c50a..03f5671 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -26,9 +26,9 @@ func (s Scene) Apply(serviceData any) { ServiceData: serviceData, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -43,9 +43,9 @@ func (s Scene) Create(entityID string, serviceData any) { ServiceData: serviceData, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -56,9 +56,9 @@ func (s Scene) Reload() { Service: "reload", } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -73,8 +73,8 @@ func (s Scene) TurnOn(entityID string, serviceData any) { ServiceData: serviceData, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/script.go b/internal/services/script.go index 7f29a9a..467d4be 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -28,9 +28,9 @@ func (s Script) Reload(entityID string) { }, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -44,9 +44,9 @@ func (s Script) Toggle(entityID string) { }, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -57,9 +57,9 @@ func (s Script) TurnOff() { Service: "turn_off", } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -73,8 +73,8 @@ func (s Script) TurnOn(entityID string) { }, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/switch.go b/internal/services/switch.go index 9a0ac91..9176f28 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -27,9 +27,9 @@ func (s Switch) TurnOn(entityID string) { }, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -42,9 +42,9 @@ func (s Switch) Toggle(entityID string) { }, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -57,8 +57,8 @@ func (s Switch) TurnOff(entityID string) { }, } - s.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + s.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/tts.go b/internal/services/tts.go index 98fed8a..29c599f 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -25,9 +25,9 @@ func (tts TTS) ClearCache() { Service: "clear_cache", } - tts.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + tts.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -42,9 +42,9 @@ func (tts TTS) CloudSay(entityID string, serviceData any) { ServiceData: serviceData, } - tts.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + tts.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -60,8 +60,8 @@ func (tts TTS) GoogleTranslateSay(entityID string, serviceData any) { ServiceData: serviceData, } - tts.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + tts.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 90288f1..32b989c 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -28,9 +28,9 @@ func (v Vacuum) CleanSpot(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -44,9 +44,9 @@ func (v Vacuum) Locate(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -60,9 +60,9 @@ func (v Vacuum) Pause(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -76,9 +76,9 @@ func (v Vacuum) ReturnToBase(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -93,9 +93,9 @@ func (v Vacuum) SendCommand(entityID string, serviceData any) { ServiceData: serviceData, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -110,9 +110,9 @@ func (v Vacuum) SetFanSpeed(entityID string, serviceData any) { ServiceData: serviceData, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -126,9 +126,9 @@ func (v Vacuum) Start(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -142,9 +142,9 @@ func (v Vacuum) StartPause(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -158,9 +158,9 @@ func (v Vacuum) Stop(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -174,9 +174,9 @@ func (v Vacuum) TurnOff(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } @@ -190,8 +190,8 @@ func (v Vacuum) TurnOn(entityID string) { }, } - v.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + v.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index e9711a1..c0fe45b 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -32,8 +32,8 @@ func (zw ZWaveJS) BulkSetPartialConfigParam(entityID string, parameter int, valu }, } - zw.conn.Send(func(mw websocket.MessageWriter) error { - req.ID = mw.NextID() - return mw.SendMessage(req) + zw.conn.Send(func(lc websocket.LockedConn) error { + req.ID = lc.NextID() + return lc.SendMessage(req) }) } diff --git a/internal/websocket/locked_conn.go b/internal/websocket/locked_conn.go new file mode 100644 index 0000000..a93df56 --- /dev/null +++ b/internal/websocket/locked_conn.go @@ -0,0 +1,10 @@ +package websocket + +// LockedConn represents a `Conn` object that is currently locked. It +// allows user access to operations that usually require the lock. +type LockedConn interface { + NextID() int64 + Subscribe(subscriber Subscriber) Subscription + Unsubscribe(subscription Subscription) + SendMessage(msg any) error +} diff --git a/internal/websocket/read.go b/internal/websocket/read.go index fb8c636..6aa3702 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -38,10 +38,10 @@ func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscrip EventType: eventType, } var subscription Subscription - err := conn.Send(func(mw MessageWriter) error { - subscription = mw.Subscribe(subscriber) + err := conn.Send(func(lc LockedConn) error { + subscription = lc.Subscribe(subscriber) e.ID = subscription.ID() - if err := mw.SendMessage(e); err != nil { + if err := lc.SendMessage(e); err != nil { conn.unsubscribe(subscription.ID()) return fmt.Errorf("error writing to websocket: %w", err) } @@ -65,9 +65,9 @@ func (conn *Conn) unwatchEvents(subscriptionID int64) error { Subscription: subscriptionID, } - err := conn.Send(func(mw MessageWriter) error { - e.ID = mw.NextID() - return mw.SendMessage(e) + err := conn.Send(func(lc LockedConn) error { + e.ID = lc.NextID() + return lc.SendMessage(e) }) if err != nil { return fmt.Errorf("unsubscribing from ID %d: %w", subscriptionID, err) diff --git a/internal/websocket/send.go b/internal/websocket/send.go index 6b900ac..f01d567 100644 --- a/internal/websocket/send.go +++ b/internal/websocket/send.go @@ -1,19 +1,14 @@ package websocket -import "fmt" - -type MessageWriter interface { - NextID() int64 - Subscribe(subscriber Subscriber) Subscription - Unsubscribe(subscription Subscription) - SendMessage(msg any) error -} +import ( + "fmt" +) // Messager is called by `Send()` while holding the `writeMutex`. It -// can send a message by allocating an ID using `mw.NextID()` then -// sending it using `mw.SendMessage()`. The `MessageWriter` should +// can send a message by allocating an ID using `lc.NextID()` then +// sending it using `lc.SendMessage()`. The `MessageWriter` should // only be used while the callback is running. -type Messager func(mw MessageWriter) error +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 @@ -25,48 +20,50 @@ type Messager func(mw MessageWriter) error // Usage: // // msg := NewFooMessage{…} -// err := conn.Send(func(mw MessageWriter) error { -// id := mw.NextID() +// err := conn.Send(func(lc MessageWriter) error { +// id := lc.NextID() // // …do anything else that needs to be done with `id`… // msg.ID = id -// return mw.SendMessage(msg) +// return lc.SendMessage(msg) // }) func (conn *Conn) Send(msgr Messager) error { conn.writeMutex.Lock() defer conn.writeMutex.Unlock() - return msgr(connMessageWriter{conn: conn}) + return msgr(lockedConn{conn: conn}) +} + +// 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 } // 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. -func (mw connMessageWriter) SendMessage(msg any) error { - if err := mw.conn.conn.WriteJSON(msg); err != nil { +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 } -type connMessageWriter struct { - conn *Conn -} - -func (mw connMessageWriter) NextID() int64 { - mw.conn.lastID++ - return mw.conn.lastID +func (lc lockedConn) NextID() int64 { + lc.conn.lastID++ + return lc.conn.lastID } // Subscribe creates a new (unique) subscription ID and subscribes // `subscriber` to it, in the sense that the subscriber will be called // for any responses that have that ID. This doesn't actually interact // with the server. -func (mw connMessageWriter) Subscribe(subscriber Subscriber) Subscription { - id := mw.NextID() - mw.conn.subscribers[id] = subscriber +func (lc lockedConn) Subscribe(subscriber Subscriber) Subscription { + id := lc.NextID() + lc.conn.subscribers[id] = subscriber return Subscription{ - conn: mw.conn, + conn: lc.conn, id: id, } } @@ -76,7 +73,7 @@ func (mw connMessageWriter) Subscribe(subscriber Subscriber) Subscription { // `Subscriber`. Note that this does not interact with the server; it // is the caller's responsibility to send it an "unsubscribe" command // if necessary. -func (mw connMessageWriter) Unsubscribe(subscription Subscription) { +func (lc lockedConn) Unsubscribe(subscription Subscription) { if subscription.id == 0 { return } From 4e4a39f9630bc1099c9f6bbbaf51b5cedab400e5 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 19:29:04 +0200 Subject: [PATCH 053/103] Move subscription-related code to a separate file --- internal/websocket/read.go | 19 -------- internal/websocket/send.go | 4 +- internal/websocket/subscriptions.go | 68 +++++++++++++++++++++++++++++ internal/websocket/websocket.go | 31 ------------- 4 files changed, 71 insertions(+), 51 deletions(-) create mode 100644 internal/websocket/subscriptions.go diff --git a/internal/websocket/read.go b/internal/websocket/read.go index 6aa3702..de50a75 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -6,25 +6,6 @@ import ( "log/slog" ) -// unsubscribe unsubscribes from `subscription`. It must be called -// exactly once for each subscription. It must be invoked while -// holding the `subscribeMutex` for writing. -func (conn *Conn) unsubscribe(id int64) error { - if _, ok := conn.subscribers[id]; !ok { - return fmt.Errorf("subscription ID %d wasn't active", id) - } - delete(conn.subscribers, id) - return nil -} - -func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { - conn.subscribeMutex.RLock() - defer conn.subscribeMutex.RUnlock() - - subscriber, ok := conn.subscribers[id] - return subscriber, ok -} - // WatchEvents subscribes to events of the given type, invoking // `subscriber` when any such events are received. Calls to // `subscriber` are synchronous with respect to any other received diff --git a/internal/websocket/send.go b/internal/websocket/send.go index f01d567..ff7e9ab 100644 --- a/internal/websocket/send.go +++ b/internal/websocket/send.go @@ -61,7 +61,9 @@ func (lc lockedConn) NextID() int64 { // with the server. func (lc lockedConn) Subscribe(subscriber Subscriber) Subscription { id := lc.NextID() - lc.conn.subscribers[id] = subscriber + if err := lc.conn.subscribe(id, subscriber); err != nil { + panic(fmt.Sprintf("newly-created ID %d is already subscribed", id)) + } return Subscription{ conn: lc.conn, id: id, diff --git a/internal/websocket/subscriptions.go b/internal/websocket/subscriptions.go new file mode 100644 index 0000000..3b776b3 --- /dev/null +++ b/internal/websocket/subscriptions.go @@ -0,0 +1,68 @@ +package websocket + +import ( + "fmt" +) + +// subscribe registers `subscriber` to be called for any messages that +// have the specified `id`. This method doesn't actually interact with +// the server; that is the caller's responsibility. This method must +// be invoked while holding the `subscribeMutex` for writing. +func (conn *Conn) subscribe(id int64, subscriber Subscriber) error { + if _, ok := conn.subscribers[id]; ok { + return fmt.Errorf("id %d is already subscribed to", id) + } + conn.subscribers[id] = subscriber + return nil +} + +func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { + conn.subscribeMutex.RLock() + defer conn.subscribeMutex.RUnlock() + + subscriber, ok := conn.subscribers[id] + return subscriber, ok +} + +// unsubscribe unsubscribes whatever subscriber is listening to +// `subscription`. It must be called exactly once for each +// subscription. It must be invoked while holding the `subscribeMutex` +// for writing. +func (conn *Conn) unsubscribe(id int64) error { + if _, ok := conn.subscribers[id]; !ok { + return fmt.Errorf("subscription ID %d wasn't active", id) + } + delete(conn.subscribers, id) + return nil +} + +// Subscriber is called synchronously when a message with the +// subscribed `id` is received. +type Subscriber func(msg Message) + +// Subscription represents a websocket-level subscription to a +// particular message ID. Incoming messages with that ID will be +// forwarded to the corresponding `Subscriber`. +type Subscription struct { + conn *Conn + id int64 +} + +func (subscription Subscription) ID() int64 { + return subscription.id +} + +func (subscription *Subscription) Cancel() { + if subscription.id == 0 { + return + } + + subscription.conn.subscribeMutex.Lock() + defer subscription.conn.subscribeMutex.Unlock() + + subscription.conn.unsubscribe(subscription.id) + + subscription.conn.unwatchEvents(subscription.id) + + subscription.id = 0 +} diff --git a/internal/websocket/websocket.go b/internal/websocket/websocket.go index 4d3e180..faf64ed 100644 --- a/internal/websocket/websocket.go +++ b/internal/websocket/websocket.go @@ -29,37 +29,6 @@ type Conn struct { lastID int64 } -// Subscriber is called synchronously when a message with the -// subscribed `id` is received. -type Subscriber func(msg Message) - -// Subscription represents a websocket-level subscription to a -// particular message ID. Incoming messages with that ID will be -// forwarded to the corresponding `Subscriber`. -type Subscription struct { - conn *Conn - id int64 -} - -func (subscription Subscription) ID() int64 { - return subscription.id -} - -func (subscription *Subscription) Cancel() { - if subscription.id == 0 { - return - } - - subscription.conn.subscribeMutex.Lock() - defer subscription.conn.subscribeMutex.Unlock() - - subscription.conn.unsubscribe(subscription.id) - - subscription.conn.unwatchEvents(subscription.id) - - subscription.id = 0 -} - func NewConnFromURI(ctx context.Context, uri string, authToken string) (*Conn, error) { // Init websocket connection dialer := websocket.DefaultDialer From 897b49c5fe7034a7ee5c6cb31914eb94eaa0228a Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 19:55:41 +0200 Subject: [PATCH 054/103] Subscription.Cancel(): remove method Instead, the caller should pass the `Subscription` object to `Locked.Conn.Unsubscribe()`. --- app.go | 79 +++++++++++++++++++++++++++-- internal/websocket/message.go | 10 ---- internal/websocket/read.go | 57 --------------------- internal/websocket/subscriptions.go | 15 ------ 4 files changed, 76 insertions(+), 85 deletions(-) diff --git a/app.go b/app.go index 079d0c4..ffeac77 100644 --- a/app.go +++ b/app.go @@ -240,7 +240,7 @@ func (app *App) RegisterEventListeners(evls ...EventListener) { if !ok { // FIXME: keep track of subscriptions so that they can // be unsubscribed from. - _, err := app.wsConn.WatchEvents( + _, err := app.WatchEvents( eventType, func(msg websocket.Message) { go app.callEventListeners(msg) @@ -325,7 +325,7 @@ func (app *App) Start(ctx context.Context) error { }) // subscribe to state_changed events - stateChangedSubscription, err := app.wsConn.WatchStateChangedEvents( + stateChangedSubscription, err := app.WatchStateChangedEvents( func(msg websocket.Message) { go app.callEntityListeners(msg) }, @@ -334,7 +334,7 @@ func (app *App) Start(ctx context.Context) error { return fmt.Errorf("subscribing to 'state_changed' events: %w", err) } - defer stateChangedSubscription.Cancel() + defer app.unwatchEvents(stateChangedSubscription) // entity listeners runOnStartup for eid, etls := range app.entityListeners { @@ -448,3 +448,76 @@ func (app *App) requeueScheduledAction(action scheduledAction) { action.updateNextRunTime(app) app.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) } + +type SubEvent struct { + websocket.BaseMessage + EventType string `json:"event_type"` +} + +// WatchEvents subscribes to events of the given type, invoking +// `subscriber` when any such events are received. Calls to +// `subscriber` are synchronous with respect to any other received +// messages, but asynchronous with respect to writes. +func (app *App) WatchEvents( + eventType string, subscriber websocket.Subscriber, +) (websocket.Subscription, error) { + // Make sure we're listening before events might start arriving: + e := SubEvent{ + BaseMessage: websocket.BaseMessage{ + Type: "subscribe_events", + }, + EventType: eventType, + } + var subscription websocket.Subscription + err := app.wsConn.Send(func(lc websocket.LockedConn) error { + subscription = lc.Subscribe(subscriber) + e.ID = subscription.ID() + if err := lc.SendMessage(e); err != nil { + lc.Unsubscribe(subscription) + return fmt.Errorf("error writing to websocket: %w", err) + } + return nil + }) + if err != nil { + return websocket.Subscription{}, err + } + // m, _ := ReadMessage(conn, ctx) + // log.Default().Println(string(m)) + return subscription, nil +} + +func (app *App) WatchStateChangedEvents( + subscriber websocket.Subscriber, +) (websocket.Subscription, error) { + return app.WatchEvents("state_changed", subscriber) +} + +type UnsubEvent struct { + websocket.BaseMessage + Subscription int64 `json:"subscription"` +} + +// unwatchEvents unsubscribes to events with the given `subscriptionID`. This does +// not remove the subscriber. +func (app *App) unwatchEvents(subscription websocket.Subscription) error { + e := UnsubEvent{ + BaseMessage: websocket.BaseMessage{ + Type: "unsubscribe_events", + }, + Subscription: subscription.ID(), + } + + err := app.wsConn.Send(func(lc websocket.LockedConn) error { + lc.Unsubscribe(subscription) + + e.ID = lc.NextID() + return lc.SendMessage(e) + }) + if err != nil { + return fmt.Errorf("unsubscribing from ID %d: %w", subscription.ID(), err) + } + + // m, _ := ReadMessage(conn, ctx) + // log.Default().Println(string(m)) + return nil +} diff --git a/internal/websocket/message.go b/internal/websocket/message.go index c9ff8e4..71552d0 100644 --- a/internal/websocket/message.go +++ b/internal/websocket/message.go @@ -25,13 +25,3 @@ type Message struct { // fields `Type` and `ID`, which appear in `BaseMessage`). Raw json.RawMessage } - -type SubEvent struct { - BaseMessage - EventType string `json:"event_type"` -} - -type UnsubEvent struct { - BaseMessage - Subscription int64 `json:"subscription"` -} diff --git a/internal/websocket/read.go b/internal/websocket/read.go index de50a75..e260db1 100644 --- a/internal/websocket/read.go +++ b/internal/websocket/read.go @@ -2,66 +2,9 @@ package websocket import ( "encoding/json" - "fmt" "log/slog" ) -// WatchEvents subscribes to events of the given type, invoking -// `subscriber` when any such events are received. Calls to -// `subscriber` are synchronous with respect to any other received -// messages, but asynchronous with respect to writes. -func (conn *Conn) WatchEvents(eventType string, subscriber Subscriber) (Subscription, error) { - // Make sure we're listening before events might start arriving: - e := SubEvent{ - BaseMessage: BaseMessage{ - Type: "subscribe_events", - }, - EventType: eventType, - } - var subscription Subscription - err := conn.Send(func(lc LockedConn) error { - subscription = lc.Subscribe(subscriber) - e.ID = subscription.ID() - if err := lc.SendMessage(e); err != nil { - conn.unsubscribe(subscription.ID()) - return fmt.Errorf("error writing to websocket: %w", err) - } - return nil - }) - if err != nil { - return Subscription{}, fmt.Errorf("error writing to websocket: %w", err) - } - // m, _ := ReadMessage(conn, ctx) - // log.Default().Println(string(m)) - return subscription, nil -} - -// unwatchEvents unsubscribes to events with the given `subscriptionID`. This does -// not remove the subscriber. -func (conn *Conn) unwatchEvents(subscriptionID int64) error { - e := UnsubEvent{ - BaseMessage: BaseMessage{ - Type: "unsubscribe_events", - }, - Subscription: subscriptionID, - } - - err := conn.Send(func(lc LockedConn) error { - e.ID = lc.NextID() - return lc.SendMessage(e) - }) - if err != nil { - return fmt.Errorf("unsubscribing from ID %d: %w", subscriptionID, err) - } - // m, _ := ReadMessage(conn, ctx) - // log.Default().Println(string(m)) - return nil -} - -func (conn *Conn) WatchStateChangedEvents(subscriber Subscriber) (Subscription, error) { - return conn.WatchEvents("state_changed", subscriber) -} - // Start reads JSON-formatted messages from `conn`, partly // deserializes them, and processes them. If the message ID is // currently subscribed to, invoke the subscriber for the message. If diff --git a/internal/websocket/subscriptions.go b/internal/websocket/subscriptions.go index 3b776b3..8ed4001 100644 --- a/internal/websocket/subscriptions.go +++ b/internal/websocket/subscriptions.go @@ -51,18 +51,3 @@ type Subscription struct { func (subscription Subscription) ID() int64 { return subscription.id } - -func (subscription *Subscription) Cancel() { - if subscription.id == 0 { - return - } - - subscription.conn.subscribeMutex.Lock() - defer subscription.conn.subscribeMutex.Unlock() - - subscription.conn.unsubscribe(subscription.id) - - subscription.conn.unwatchEvents(subscription.id) - - subscription.id = 0 -} From 5a3fdba7d728d6813987aa9c519a02cda9c4787e Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 20:05:56 +0200 Subject: [PATCH 055/103] Inline some subscription-related functions in `lockedConn` --- internal/websocket/locked_conn.go | 22 ++++++++++++++++++++ internal/websocket/send.go | 22 +++----------------- internal/websocket/subscriptions.go | 31 +---------------------------- 3 files changed, 26 insertions(+), 49 deletions(-) diff --git a/internal/websocket/locked_conn.go b/internal/websocket/locked_conn.go index a93df56..5043232 100644 --- a/internal/websocket/locked_conn.go +++ b/internal/websocket/locked_conn.go @@ -3,8 +3,30 @@ package websocket // LockedConn represents a `Conn` object that is currently locked. It // allows user access to operations that usually require the lock. type LockedConn interface { + // NextID 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. NextID() int64 + + // Subscribe creates a new (unique) subscription ID and subscribes + // `subscriber` to it, in the sense that the subscriber will be + // called for any responses that have that ID. This doesn't + // actually interact with the server. The returned `Subscription` + // must eventually be passed at least once to `Unsubscribe()`, + // though `Unsubscribe()` can be called against a different + // `LockedConn` than the one that generated it. Subscribe(subscriber Subscriber) Subscription + + // Unsubscribe terminates `subscription` at the websocket level; + // i.e., no more incoming messages will be forwarded to the + // corresponding `Subscriber`. Note that this does not interact + // with the server; it is the caller's responsibility to send it + // an "unsubscribe" command if necessary. Unsubscribe(subscription Subscription) + + // 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 `NextID()` and used in order. SendMessage(msg any) error } diff --git a/internal/websocket/send.go b/internal/websocket/send.go index ff7e9ab..ce1e502 100644 --- a/internal/websocket/send.go +++ b/internal/websocket/send.go @@ -39,9 +39,6 @@ type lockedConn struct { conn *Conn } -// 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. 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) @@ -55,31 +52,18 @@ func (lc lockedConn) NextID() int64 { return lc.conn.lastID } -// Subscribe creates a new (unique) subscription ID and subscribes -// `subscriber` to it, in the sense that the subscriber will be called -// for any responses that have that ID. This doesn't actually interact -// with the server. func (lc lockedConn) Subscribe(subscriber Subscriber) Subscription { id := lc.NextID() - if err := lc.conn.subscribe(id, subscriber); err != nil { - panic(fmt.Sprintf("newly-created ID %d is already subscribed", id)) - } + lc.conn.subscribers[id] = subscriber return Subscription{ - conn: lc.conn, - id: id, + id: id, } } -// Unsubscribe terminates `subscription` at the websocket level; i.e., -// no more incoming messages will be forwarded to the corresponding -// `Subscriber`. Note that this does not interact with the server; it -// is the caller's responsibility to send it an "unsubscribe" command -// if necessary. func (lc lockedConn) Unsubscribe(subscription Subscription) { if subscription.id == 0 { return } - - subscription.conn.unsubscribe(subscription.id) + delete(lc.conn.subscribers, subscription.id) subscription.id = 0 } diff --git a/internal/websocket/subscriptions.go b/internal/websocket/subscriptions.go index 8ed4001..80d2f30 100644 --- a/internal/websocket/subscriptions.go +++ b/internal/websocket/subscriptions.go @@ -1,21 +1,5 @@ package websocket -import ( - "fmt" -) - -// subscribe registers `subscriber` to be called for any messages that -// have the specified `id`. This method doesn't actually interact with -// the server; that is the caller's responsibility. This method must -// be invoked while holding the `subscribeMutex` for writing. -func (conn *Conn) subscribe(id int64, subscriber Subscriber) error { - if _, ok := conn.subscribers[id]; ok { - return fmt.Errorf("id %d is already subscribed to", id) - } - conn.subscribers[id] = subscriber - return nil -} - func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { conn.subscribeMutex.RLock() defer conn.subscribeMutex.RUnlock() @@ -24,18 +8,6 @@ func (conn *Conn) getSubscriber(id int64) (Subscriber, bool) { return subscriber, ok } -// unsubscribe unsubscribes whatever subscriber is listening to -// `subscription`. It must be called exactly once for each -// subscription. It must be invoked while holding the `subscribeMutex` -// for writing. -func (conn *Conn) unsubscribe(id int64) error { - if _, ok := conn.subscribers[id]; !ok { - return fmt.Errorf("subscription ID %d wasn't active", id) - } - delete(conn.subscribers, id) - return nil -} - // Subscriber is called synchronously when a message with the // subscribed `id` is received. type Subscriber func(msg Message) @@ -44,8 +16,7 @@ type Subscriber func(msg Message) // particular message ID. Incoming messages with that ID will be // forwarded to the corresponding `Subscriber`. type Subscription struct { - conn *Conn - id int64 + id int64 } func (subscription Subscription) ID() int64 { From 307fd01d25788135a6ac34646556818eecef7a20 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 20:08:51 +0200 Subject: [PATCH 056/103] Remove a race related to variable-capture --- app.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index ffeac77..6d251b2 100644 --- a/app.go +++ b/app.go @@ -338,7 +338,9 @@ func (app *App) Start(ctx context.Context) error { // entity listeners runOnStartup for eid, etls := range app.entityListeners { + eid := eid for _, etl := range etls { + etl := etl // ensure each ETL only runs once, even if // it listens to multiple entities if etl.runOnStartup && !etl.runOnStartupCompleted { @@ -351,7 +353,6 @@ func (app *App) Start(ctx context.Context) error { } etl.runOnStartupCompleted = true - etl := etl eg.Go(func() error { etl.callback(app.service, app.state, EntityData{ TriggerEntityID: eid, From 26d94f60c52eb589fe312b2a7d09983008732d31 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 21:02:44 +0200 Subject: [PATCH 057/103] App: add convenience methods for interacting with the service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the following methods to make it easier to interact with the service, including getting results from a request: * `Call()` — send an arbitrary message, then wait for and return a single response message. * `CallService()` — send a `call_service` request, then wait for and return a single response message. * `Subscribe()` — send an arbitrary subscription message, wait for its result, but then leave a subscriber subscribed to events on that ID. --- app.go | 145 ++++++++++++++++++++++++++++++++++ internal/websocket/message.go | 13 +++ 2 files changed, 158 insertions(+) diff --git a/app.go b/app.go index 6d251b2..ac21cb2 100644 --- a/app.go +++ b/app.go @@ -14,6 +14,7 @@ import ( "saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/priorityqueue" + "saml.dev/gome-assistant/internal/services" "saml.dev/gome-assistant/internal/websocket" ) @@ -522,3 +523,147 @@ func (app *App) unwatchEvents(subscription websocket.Subscription) error { // log.Default().Println(string(m)) return nil } + +// Call invokes an RPC service corresponding to `req` via websockets +// and waits for and returns a single `result`. `msg` must be +// serializable to JSON. It shouldn't have its ID filled in yet; that +// will be done within this method. The response is not analyzed at +// all, even to check for errors. +func (app *App) Call( + ctx context.Context, req websocket.Request, +) (websocket.Message, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + responseCh := make(chan websocket.Message, 1) + + var subscription websocket.Subscription + + // Receive a single message, sent it to `responseCh`, then + // unsubscribe: + subscriber := func(msg websocket.Message) { + defer close(responseCh) + responseCh <- msg + _ = app.wsConn.Send(func(lc websocket.LockedConn) error { + lc.Unsubscribe(subscription) + return nil + }) + } + + err := app.wsConn.Send(func(lc websocket.LockedConn) error { + subscription = lc.Subscribe(subscriber) + req.SetID(subscription.ID()) + if err := lc.SendMessage(req); err != nil { + lc.Unsubscribe(subscription) + return fmt.Errorf("error writing to websocket: %w", err) + } + return nil + }) + + if err != nil { + return websocket.Message{}, err + } + + select { + case response := <-responseCh: + return response, nil + case <-ctx.Done(): + return websocket.Message{}, ctx.Err() + } +} + +type CallServiceRequest2 struct { + websocket.BaseMessage + Domain string `json:"domain"` + Service string `json:"service"` + + // ServiceData must be serializable to a JSON object. + ServiceData any `json:"service_data,omitempty"` + + Target services.Target `json:"target,omitempty"` +} + +// CallService invokes a service using a `call_service` message, then +// waits for and returns the response. +// +// FIXME: can the response be parsed into a result-style message? +func (app *App) CallService( + ctx context.Context, domain string, service string, serviceData any, target services.Target, +) (websocket.Message, error) { + req := CallServiceRequest2{ + BaseMessage: websocket.BaseMessage{ + Type: "call_service", + }, + Domain: domain, + Service: service, + ServiceData: serviceData, + Target: target, + } + + return app.Call(ctx, &req) +} + +// Subscribe subscribes to some events via `req`, waits for a single +// response, and then leaves `subscriber` subscribed to the events. If +// this method returns without an error, `subscriber` must eventually +// be unsubscribed. `ctx` covers the subscription and the wait for the +// first answer, but not the forwarding of subsequent events or +// unsubscribing. +// +// FIXME: should this subscriber and subscription be specialized to +// event messages? +// +// FIXME: should the result be examined? If the subscription request +// failed, then we could fail more generally instead of leaving the +// cleanup to the caller. +func (app *App) Subscribe( + ctx context.Context, req websocket.Request, subscriber websocket.Subscriber, +) (websocket.Message, websocket.Subscription, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + // The result of the attempt to subscribe (i.e., the first + // message) will be sent to this channel. + resultReceived := false + resultCh := make(chan websocket.Message, 1) + + var subscription websocket.Subscription + + // Receive a single message, sent it to `responseCh`, then + // unsubscribe: + dualSubscriber := func(msg websocket.Message) { + if !resultReceived { + // This is the first message. We send it to the channel so + // that it can be returned from the outer function. + defer close(resultCh) + resultCh <- msg + resultReceived = true + return + } + + // The result has already been processed. Subsequent events + // get forwarded to `subscriber`: + subscriber(msg) + } + + err := app.wsConn.Send(func(lc websocket.LockedConn) error { + subscription = lc.Subscribe(dualSubscriber) + req.SetID(subscription.ID()) + if err := lc.SendMessage(req); err != nil { + lc.Unsubscribe(subscription) + return fmt.Errorf("error writing to websocket: %w", err) + } + return nil + }) + + if err != nil { + return websocket.Message{}, websocket.Subscription{}, err + } + + select { + case response := <-resultCh: + return response, subscription, nil + case <-ctx.Done(): + return websocket.Message{}, websocket.Subscription{}, ctx.Err() + } +} diff --git a/internal/websocket/message.go b/internal/websocket/message.go index 71552d0..e43deae 100644 --- a/internal/websocket/message.go +++ b/internal/websocket/message.go @@ -11,6 +11,19 @@ type BaseMessage struct { ID int64 `json:"id"` } +type Request interface { + GetID() int64 + SetID(id int64) +} + +func (msg *BaseMessage) GetID() int64 { + return msg.ID +} + +func (msg *BaseMessage) SetID(id int64) { + msg.ID = id +} + type BaseResultMessage struct { BaseMessage Success bool `json:"success"` From 26bb7b72850d7d61b6d5ab6f8b6e4f0ade07cda0 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 21:33:43 +0200 Subject: [PATCH 058/103] Change a couple of services to use the new call mechanism --- app.go | 10 +++--- internal/services/event.go | 24 +++++++------ internal/services/light.go | 64 +++++++++++------------------------ internal/services/services.go | 32 +++++++++++++++--- service.go | 8 ++--- 5 files changed, 69 insertions(+), 69 deletions(-) diff --git a/app.go b/app.go index ac21cb2..d0240cc 100644 --- a/app.go +++ b/app.go @@ -108,22 +108,22 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { httpClient := http.ClientFromUri(config.RESTBaseURI, config.HAAuthToken) - service := newService(wsWriter, httpClient) state, err := newState(httpClient, config.HomeZoneEntityID) if err != nil { return nil, err } - - return &App{ + app := App{ wsConn: wsWriter, httpClient: httpClient, - service: service, state: state, scheduledActions: priorityqueue.New(), entityListeners: map[string][]*EntityListener{}, eventListeners: map[string][]*EventListener{}, cancel: func() {}, - }, nil + } + app.service = newService(&app, httpClient) + + return &app, nil } type NewAppRequest struct { diff --git a/internal/services/event.go b/internal/services/event.go index ca212d2..483ecc2 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -1,23 +1,24 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) type Event struct { - conn *websocket.Conn + service Service } -func NewEvent(conn *websocket.Conn) *Event { +func NewEvent(service Service) *Event { return &Event{ - conn: conn, + service: service, } } // Fire an event type FireEventRequest struct { - ID int64 `json:"id"` - Type string `json:"type"` // always set to "fire_event" + websocket.BaseMessage EventType string `json:"event_type"` EventData map[string]any `json:"event_data,omitempty"` } @@ -26,16 +27,17 @@ 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) { +func (e Event) Fire(eventType string, eventData map[string]any) (websocket.Message, error) { + ctx := context.TODO() + req := FireEventRequest{ - Type: "fire_event", + BaseMessage: websocket.BaseMessage{ + Type: "fire_event", + }, } req.EventType = eventType req.EventData = eventData - e.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) + return e.service.Call(ctx, &req) } diff --git a/internal/services/light.go b/internal/services/light.go index 7134085..bd7e5d6 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -1,68 +1,44 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Light struct { - conn *websocket.Conn + service Service } -func NewLight(conn *websocket.Conn) *Light { +func NewLight(service Service) *Light { return &Light{ - conn: conn, + service: service, } } /* Public API */ // TurnOn a light entity. -func (l Light) TurnOn(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "light", - Service: "turn_on", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - l.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (l Light) TurnOn(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return l.service.CallService( + ctx, "light", "turn_on", serviceData, EntityTarget(entityID), + ) } // Toggle a light entity. -func (l Light) Toggle(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "light", - Service: "toggle", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - l.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (l Light) Toggle(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return l.service.CallService( + ctx, "light", "toggle", serviceData, EntityTarget(entityID), + ) } -func (l Light) TurnOff(entityID string) { - req := CallServiceRequest{ - Domain: "light", - Service: "turn_off", - Target: Target{ - EntityID: entityID, - }, - } - - l.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (l Light) TurnOff(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return l.service.CallService( + ctx, "light", "turn_off", nil, EntityTarget(entityID), + ) } diff --git a/internal/services/services.go b/internal/services/services.go index f454734..21016e7 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -1,5 +1,32 @@ package services +import ( + "context" + + "saml.dev/gome-assistant/internal/websocket" +) + +// Target represents the target of the service call, if applicable. +type Target struct { + EntityID string `json:"entity_id,omitempty"` +} + +func EntityTarget(entityID string) Target { + return Target{ + EntityID: entityID, + } +} + +type Service interface { + Call( + ctx context.Context, req websocket.Request, + ) (websocket.Message, error) + + CallService( + ctx context.Context, domain string, service string, serviceData any, target Target, + ) (websocket.Message, error) +} + // CallService is a type that always serializes as `"call_service"`. type CallService struct{} @@ -11,11 +38,6 @@ func (CallService) MarshalJSON() ([]byte, error) { return []byte(`"call_service"`), nil } -// Target represents the target of the service call, if applicable. -type Target struct { - EntityID string `json:"entity_id,omitempty"` -} - type CallServiceRequest struct { ID int64 `json:"id"` RequestType CallService `json:"type"` // hardcoded "call_service" diff --git a/service.go b/service.go index 78cbd61..d473047 100644 --- a/service.go +++ b/service.go @@ -3,7 +3,6 @@ package gomeassistant import ( "saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/services" - "saml.dev/gome-assistant/internal/websocket" ) type Service struct { @@ -30,12 +29,13 @@ type Service struct { ZWaveJS *services.ZWaveJS } -func newService(conn *websocket.Conn, httpClient *http.HttpClient) *Service { +func newService(app *App, httpClient *http.HttpClient) *Service { + conn := app.wsConn return &Service{ AlarmControlPanel: services.NewAlarmControlPanel(conn), Climate: services.NewClimate(conn), Cover: services.NewCover(conn), - Light: services.NewLight(conn), + Light: services.NewLight(app), HomeAssistant: services.NewHomeAssistant(conn), Lock: services.NewLock(conn), MediaPlayer: services.NewMediaPlayer(conn), @@ -45,7 +45,7 @@ func newService(conn *websocket.Conn, httpClient *http.HttpClient) *Service { InputText: services.NewInputText(conn), InputDatetime: services.NewInputDatetime(conn), InputNumber: services.NewInputNumber(conn), - Event: services.NewEvent(conn), + Event: services.NewEvent(app), Notify: services.NewNotify(conn), Number: services.NewNumber(conn), Scene: services.NewScene(conn), From f5833fccf70ea2dd5863db21eb00e283bfcc9b2a Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 22:56:06 +0200 Subject: [PATCH 059/103] Switch to the new way of calling services --- app.go | 4 +- internal/services/alarm_control_panel.go | 162 +++----- internal/services/climate.go | 54 ++- internal/services/cover.go | 200 ++++------ internal/services/homeassistant.go | 67 ++-- internal/services/input_boolean.go | 80 ++-- internal/services/input_button.go | 42 +- internal/services/input_datetime.go | 43 +-- internal/services/input_number.go | 82 ++-- internal/services/input_text.go | 44 +-- internal/services/lock.go | 48 +-- internal/services/media_player.go | 470 +++++++---------------- internal/services/notify.go | 25 +- internal/services/number.go | 29 +- internal/services/scene.go | 80 ++-- internal/services/script.go | 81 ++-- internal/services/services.go | 23 -- internal/services/switch.go | 65 ++-- internal/services/tts.go | 63 ++- internal/services/vacuum.go | 219 ++++------- internal/services/zwavejs.go | 31 +- service.go | 39 +- 22 files changed, 668 insertions(+), 1283 deletions(-) diff --git a/app.go b/app.go index d0240cc..8b851ff 100644 --- a/app.go +++ b/app.go @@ -572,7 +572,7 @@ func (app *App) Call( } } -type CallServiceRequest2 struct { +type CallServiceRequest struct { websocket.BaseMessage Domain string `json:"domain"` Service string `json:"service"` @@ -590,7 +590,7 @@ type CallServiceRequest2 struct { func (app *App) CallService( ctx context.Context, domain string, service string, serviceData any, target services.Target, ) (websocket.Message, error) { - req := CallServiceRequest2{ + req := CallServiceRequest{ BaseMessage: websocket.BaseMessage{ Type: "call_service", }, diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 95c5436..1820080 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -1,142 +1,102 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type AlarmControlPanel struct { - conn *websocket.Conn + service Service } /* Public API */ -func NewAlarmControlPanel(conn *websocket.Conn) *AlarmControlPanel { +func NewAlarmControlPanel(service Service) *AlarmControlPanel { return &AlarmControlPanel{ - conn: conn, + service: service, } } // Send the alarm the command for arm away. -func (acp AlarmControlPanel) ArmAway(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "alarm_control_panel", - Service: "alarm_arm_away", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - acp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (acp AlarmControlPanel) ArmAway( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return acp.service.CallService( + ctx, "alarm_control_panel", "alarm_arm_away", + serviceData, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "alarm_control_panel", - Service: "alarm_arm_custom_bypass", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - acp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (acp AlarmControlPanel) ArmWithCustomBypass( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return acp.service.CallService( + ctx, "alarm_control_panel", "alarm_arm_custom_bypass", + serviceData, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "alarm_control_panel", - Service: "alarm_arm_home", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - acp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (acp AlarmControlPanel) ArmHome( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return acp.service.CallService( + ctx, "alarm_control_panel", "alarm_arm_home", + serviceData, EntityTarget(entityID), + ) } // Send the alarm the command for arm night. -func (acp AlarmControlPanel) ArmNight(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "alarm_control_panel", - Service: "alarm_arm_night", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - acp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (acp AlarmControlPanel) ArmNight( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return acp.service.CallService( + ctx, "alarm_control_panel", "alarm_arm_night", + serviceData, EntityTarget(entityID), + ) } // Send the alarm the command for arm vacation. -func (acp AlarmControlPanel) ArmVacation(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "alarm_control_panel", - Service: "alarm_arm_vacation", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - acp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (acp AlarmControlPanel) ArmVacation( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return acp.service.CallService( + ctx, "alarm_control_panel", "alarm_arm_vacation", + serviceData, EntityTarget(entityID), + ) } // Send the alarm the command for disarm. -func (acp AlarmControlPanel) Disarm(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "alarm_control_panel", - Service: "alarm_disarm", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - acp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (acp AlarmControlPanel) Disarm( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return acp.service.CallService( + ctx, "alarm_control_panel", "alarm_disarm", + serviceData, EntityTarget(entityID), + ) } // Send the alarm the command for trigger. -func (acp AlarmControlPanel) Trigger(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "alarm_control_panel", - Service: "alarm_trigger", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - acp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (acp AlarmControlPanel) Trigger( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return acp.service.CallService( + ctx, "alarm_control_panel", "alarm_trigger", + serviceData, EntityTarget(entityID), + ) } diff --git a/internal/services/climate.go b/internal/services/climate.go index a9d2e35..2adf45d 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -1,35 +1,32 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Climate struct { - conn *websocket.Conn + service Service } -func NewClimate(conn *websocket.Conn) *Climate { +func NewClimate(service Service) *Climate { return &Climate{ - conn: conn, + service: service, } } -func (c Climate) SetFanMode(entityID string, fanMode string) { - req := CallServiceRequest{ - Domain: "climate", - Service: "set_fan_mode", - Target: Target{ - EntityID: entityID, - }, - ServiceData: map[string]any{"fan_mode": fanMode}, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Climate) SetFanMode( + entityID string, fanMode string, +) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "climate", "set_fan_mode", + map[string]any{"fan_mode": fanMode}, + EntityTarget(entityID), + ) } type SetTemperatureRequest struct { @@ -56,18 +53,13 @@ func (r *SetTemperatureRequest) ToJSON() map[string]any { return m } -func (c Climate) SetTemperature(entityID string, setTemperatureRequest SetTemperatureRequest) { - req := CallServiceRequest{ - Domain: "climate", - Service: "set_temperature", - Target: Target{ - EntityID: entityID, - }, - ServiceData: setTemperatureRequest.ToJSON(), - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Climate) SetTemperature( + entityID string, setTemperatureRequest SetTemperatureRequest, +) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "climate", "set_temperature", + setTemperatureRequest.ToJSON(), + EntityTarget(entityID), + ) } diff --git a/internal/services/cover.go b/internal/services/cover.go index c732966..1da3267 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -1,183 +1,113 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Cover struct { - conn *websocket.Conn + service Service } -func NewCover(conn *websocket.Conn) *Cover { +func NewCover(service Service) *Cover { return &Cover{ - conn: conn, + service: service, } } /* Public API */ // Close all or specified cover. Takes an entityID. -func (c Cover) Close(entityID string) { - req := CallServiceRequest{ - Domain: "cover", - Service: "close_cover", - Target: Target{ - EntityID: entityID, - }, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) Close(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "close_cover", + nil, EntityTarget(entityID), + ) } // Close all or specified cover tilt. Takes an entityID. -func (c Cover) CloseTilt(entityID string) { - req := CallServiceRequest{ - Domain: "cover", - Service: "close_cover_tilt", - Target: Target{ - EntityID: entityID, - }, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) CloseTilt(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "close_cover_tilt", + nil, EntityTarget(entityID), + ) } // Open all or specified cover. Takes an entityID. -func (c Cover) Open(entityID string) { - req := CallServiceRequest{ - Domain: "cover", - Service: "open_cover", - Target: Target{ - EntityID: entityID, - }, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) Open(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "open_cover", + nil, EntityTarget(entityID), + ) } // Open all or specified cover tilt. Takes an entityID. -func (c Cover) OpenTilt(entityID string) { - req := CallServiceRequest{ - Domain: "cover", - Service: "open_cover_tilt", - Target: Target{ - EntityID: entityID, - }, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) OpenTilt(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "open_cover_tilt", + nil, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "cover", - Service: "set_cover_position", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) SetPosition(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "set_cover_position", + serviceData, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "cover", - Service: "set_cover_tilt_position", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) SetTiltPosition(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "set_cover_tilt_position", + serviceData, EntityTarget(entityID), + ) } // Stop a cover entity. Takes an entityID. -func (c Cover) Stop(entityID string) { - req := CallServiceRequest{ - Domain: "cover", - Service: "stop_cover", - Target: Target{ - EntityID: entityID, - }, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) Stop(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "stop_cover", + nil, EntityTarget(entityID), + ) } // Stop a cover entity tilt. Takes an entityID. -func (c Cover) StopTilt(entityID string) { - req := CallServiceRequest{ - Domain: "cover", - Service: "stop_cover_tilt", - Target: Target{ - EntityID: entityID, - }, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) StopTilt(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "stop_cover_tilt", + nil, EntityTarget(entityID), + ) } // Toggle a cover open/closed. Takes an entityID. -func (c Cover) Toggle(entityID string) { - req := CallServiceRequest{ - Domain: "cover", - Service: "toggle", - Target: Target{ - EntityID: entityID, - }, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) Toggle(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "toggle", + nil, EntityTarget(entityID), + ) } // Toggle a cover tilt open/closed. Takes an entityID. -func (c Cover) ToggleTilt(entityID string) { - req := CallServiceRequest{ - Domain: "cover", - Service: "toggle_cover_tilt", - Target: Target{ - EntityID: entityID, - }, - } - - c.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (c Cover) ToggleTilt(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return c.service.CallService( + ctx, "cover", "toggle_cover_tilt", + nil, EntityTarget(entityID), + ) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 9c63428..8e75669 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -1,66 +1,45 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) type HomeAssistant struct { - conn *websocket.Conn + service Service } -func NewHomeAssistant(conn *websocket.Conn) *HomeAssistant { +func NewHomeAssistant(service Service) *HomeAssistant { return &HomeAssistant{ - conn: conn, + service: service, } } // 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 any) { - req := CallServiceRequest{ - Domain: "homeassistant", - Service: "turn_on", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - ha.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ha *HomeAssistant) TurnOn(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return ha.service.CallService( + ctx, "homeassistant", "turn_on", + serviceData, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "homeassistant", - Service: "toggle", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - ha.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ha *HomeAssistant) Toggle(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return ha.service.CallService( + ctx, "homeassistant", "toggle", + serviceData, EntityTarget(entityID), + ) } -func (ha *HomeAssistant) TurnOff(entityID string) { - req := CallServiceRequest{ - Domain: "homeassistant", - Service: "turn_off", - Target: Target{ - EntityID: entityID, - }, - } - - ha.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ha *HomeAssistant) TurnOff(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return ha.service.CallService( + ctx, "homeassistant", "turn_off", + nil, EntityTarget(entityID), + ) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 363b83d..92beb22 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -1,76 +1,52 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputBoolean struct { - conn *websocket.Conn + service Service } -func NewInputBoolean(conn *websocket.Conn) *InputBoolean { +func NewInputBoolean(service Service) *InputBoolean { return &InputBoolean{ - conn: conn, + service: service, } } /* Public API */ -func (ib InputBoolean) TurnOn(entityID string) { - req := CallServiceRequest{ - Domain: "input_boolean", - Service: "turn_on", - Target: Target{ - EntityID: entityID, - }, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputBoolean) TurnOn(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_boolean", "turn_on", + nil, EntityTarget(entityID), + ) } -func (ib InputBoolean) Toggle(entityID string) { - req := CallServiceRequest{ - Domain: "input_boolean", - Service: "toggle", - Target: Target{ - EntityID: entityID, - }, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputBoolean) Toggle(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_boolean", "toggle", + nil, EntityTarget(entityID), + ) } -func (ib InputBoolean) TurnOff(entityID string) { - req := CallServiceRequest{ - Domain: "input_boolean", - Service: "turn_off", - Target: Target{ - EntityID: entityID, - }, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputBoolean) TurnOff(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_boolean", "turn_off", + nil, EntityTarget(entityID), + ) } -func (ib InputBoolean) Reload() { - req := CallServiceRequest{ - Domain: "input_boolean", - Service: "reload", - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputBoolean) Reload() (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_boolean", "reload", nil, Target{}, + ) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 5bb880f..69a57a6 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -1,46 +1,36 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputButton struct { - conn *websocket.Conn + service Service } -func NewInputButton(conn *websocket.Conn) *InputButton { +func NewInputButton(service Service) *InputButton { return &InputButton{ - conn: conn, + service: service, } } /* Public API */ -func (ib InputButton) Press(entityID string) { - req := CallServiceRequest{ - Domain: "input_button", - Service: "press", - Target: Target{ - EntityID: entityID, - }, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputButton) Press(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_button", "press", + nil, EntityTarget(entityID), + ) } -func (ib InputButton) Reload() { - req := CallServiceRequest{ - Domain: "input_button", - Service: "reload", - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputButton) Reload() (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_button", "reload", nil, Target{}, + ) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index bca19e8..6c8f1b5 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -1,6 +1,7 @@ package services import ( + "context" "fmt" "time" @@ -10,43 +11,31 @@ import ( /* Structs */ type InputDatetime struct { - conn *websocket.Conn + service Service } -func NewInputDatetime(conn *websocket.Conn) *InputDatetime { +func NewInputDatetime(service Service) *InputDatetime { return &InputDatetime{ - conn: conn, + service: service, } } /* Public API */ -func (ib InputDatetime) Set(entityID string, value time.Time) { - req := CallServiceRequest{ - Domain: "input_datetime", - Service: "set_datetime", - Target: Target{ - EntityID: entityID, - }, - ServiceData: map[string]any{ +func (ib InputDatetime) Set(entityID string, value time.Time) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_datetime", "set_datetime", + map[string]any{ "timestamp": fmt.Sprint(value.Unix()), }, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) + EntityTarget(entityID), + ) } -func (ib InputDatetime) Reload() { - req := CallServiceRequest{ - Domain: "input_datetime", - Service: "reload", - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputDatetime) Reload() (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_datetime", "reload", nil, Target{}, + ) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index e7c818f..73e25f5 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -1,77 +1,53 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputNumber struct { - conn *websocket.Conn + service Service } -func NewInputNumber(conn *websocket.Conn) *InputNumber { +func NewInputNumber(service Service) *InputNumber { return &InputNumber{ - conn: conn, + service: service, } } /* Public API */ -func (ib InputNumber) Set(entityID string, value float32) { - req := CallServiceRequest{ - Domain: "input_number", - Service: "set_value", - Target: Target{ - EntityID: entityID, - }, - ServiceData: map[string]any{"value": value}, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputNumber) Set(entityID string, value float32) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_number", "set_value", + map[string]any{"value": value}, + EntityTarget(entityID), + ) } -func (ib InputNumber) Increment(entityID string) { - req := CallServiceRequest{ - Domain: "input_number", - Service: "increment", - Target: Target{ - EntityID: entityID, - }, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputNumber) Increment(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_number", "increment", + nil, EntityTarget(entityID), + ) } -func (ib InputNumber) Decrement(entityID string) { - req := CallServiceRequest{ - Domain: "input_number", - Service: "decrement", - Target: Target{ - EntityID: entityID, - }, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputNumber) Decrement(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_number", "decrement", + nil, EntityTarget(entityID), + ) } -func (ib InputNumber) Reload() { - req := CallServiceRequest{ - Domain: "input_number", - Service: "reload", - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputNumber) Reload() (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_number", "reload", nil, Target{}, + ) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 45b1e06..a1bb377 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -1,49 +1,39 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type InputText struct { - conn *websocket.Conn + service Service } -func NewInputText(conn *websocket.Conn) *InputText { +func NewInputText(service Service) *InputText { return &InputText{ - conn: conn, + service: service, } } /* Public API */ -func (ib InputText) Set(entityID string, value string) { - req := CallServiceRequest{ - Domain: "input_text", - Service: "set_value", - Target: Target{ - EntityID: entityID, - }, - ServiceData: map[string]any{ +func (ib InputText) Set(entityID string, value string) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_text", "set_value", + map[string]any{ "value": value, }, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) + EntityTarget(entityID), + ) } -func (ib InputText) Reload() { - req := CallServiceRequest{ - Domain: "input_text", - Service: "reload", - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib InputText) Reload() (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "input_text", "reload", nil, Target{}, + ) } diff --git a/internal/services/lock.go b/internal/services/lock.go index 1f77dfa..e15a3ba 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -1,53 +1,39 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Lock struct { - conn *websocket.Conn + service Service } -func NewLock(conn *websocket.Conn) *Lock { +func NewLock(service Service) *Lock { return &Lock{ - conn: conn, + service: service, } } /* Public API */ // Lock a lock entity. -func (l Lock) Lock(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "lock", - Service: "lock", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - l.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (l Lock) Lock(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return l.service.CallService( + ctx, "lock", "lock", + serviceData, EntityTarget(entityID), + ) } // Unlock a lock entity. -func (l Lock) Unlock(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "lock", - Service: "unlock", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - l.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (l Lock) Unlock(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return l.service.CallService( + ctx, "lock", "unlock", + serviceData, EntityTarget(entityID), + ) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 0b8e263..ffc512f 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -1,410 +1,224 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type MediaPlayer struct { - conn *websocket.Conn + service Service } -func NewMediaPlayer(conn *websocket.Conn) *MediaPlayer { +func NewMediaPlayer(service Service) *MediaPlayer { return &MediaPlayer{ - conn: conn, + service: service, } } /* Public API */ // Send the media player the command to clear players playlist. -// Takes an entityID. -func (mp MediaPlayer) ClearPlaylist(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "clear_playlist", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) ClearPlaylist(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "clear_playlist", + nil, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "join", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Join(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "join", + serviceData, EntityTarget(entityID), + ) } // Send the media player the command for next track. -// Takes an entityID. -func (mp MediaPlayer) Next(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "media_next_track", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Next(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "media_next_track", + nil, EntityTarget(entityID), + ) } // Send the media player the command for pause. -// Takes an entityID. -func (mp MediaPlayer) Pause(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "media_pause", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Pause(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "media_pause", + nil, EntityTarget(entityID), + ) } // Send the media player the command for play. -// Takes an entityID. -func (mp MediaPlayer) Play(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "media_play", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Play(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "media_play", + nil, EntityTarget(entityID), + ) } // Toggle media player play/pause state. -// Takes an entityID. -func (mp MediaPlayer) PlayPause(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "media_play_pause", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) PlayPause(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "media_play_pause", + nil, EntityTarget(entityID), + ) } // Send the media player the command for previous track. -// Takes an entityID. -func (mp MediaPlayer) Previous(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "media_previous_track", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Previous(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "media_previous_track", + nil, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "media_seek", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Seek(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "media_seek", + serviceData, EntityTarget(entityID), + ) } // Send the media player the stop command. -// Takes an entityID. -func (mp MediaPlayer) Stop(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "media_stop", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Stop(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "media_stop", + nil, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "play_media", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) PlayMedia(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "play_media", + serviceData, EntityTarget(entityID), + ) } -// Set repeat mode. Takes an entityID and an optional -// map that is translated into service_data. -func (mp MediaPlayer) RepeatSet(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "repeat_set", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +// Set repeat mode. +func (mp MediaPlayer) RepeatSet(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "repeat_set", + serviceData, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "select_sound_mode", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) SelectSoundMode( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "select_sound_mode", + serviceData, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "select_source", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) SelectSource( + entityID string, serviceData any, +) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "select_source", + serviceData, EntityTarget(entityID), + ) } // Set shuffling state. -// Takes an entityID and an optional -// map that is translated into service_data. -func (mp MediaPlayer) Shuffle(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "shuffle_set", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Shuffle(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "shuffle_set", + serviceData, EntityTarget(entityID), + ) } // Toggles a media player power state. -// Takes an entityID. -func (mp MediaPlayer) Toggle(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "toggle", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Toggle(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "toggle", + nil, EntityTarget(entityID), + ) } // Turn a media player power off. -// Takes an entityID. -func (mp MediaPlayer) TurnOff(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "turn_off", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) TurnOff(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "turn_off", + nil, EntityTarget(entityID), + ) } // Turn a media player power on. -// Takes an entityID. -func (mp MediaPlayer) TurnOn(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "turn_on", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) TurnOn(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "turn_on", + nil, EntityTarget(entityID), + ) } // Unjoin the player from a group. Only works on // platforms with support for player groups. -// Takes an entityID. -func (mp MediaPlayer) Unjoin(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "unjoin", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) Unjoin(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "unjoin", + nil, EntityTarget(entityID), + ) } // Turn a media player volume down. -// Takes an entityID. -func (mp MediaPlayer) VolumeDown(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "volume_down", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) VolumeDown(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "volume_down", + nil, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "volume_mute", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) VolumeMute(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "volume_mute", + serviceData, EntityTarget(entityID), + ) } // 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 any) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "volume_set", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) VolumeSet(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "volume_set", + serviceData, EntityTarget(entityID), + ) } // Turn a media player volume up. -// Takes an entityID. -func (mp MediaPlayer) VolumeUp(entityID string) { - req := CallServiceRequest{ - Domain: "media_player", - Service: "volume_up", - Target: Target{ - EntityID: entityID, - }, - } - - mp.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (mp MediaPlayer) VolumeUp(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return mp.service.CallService( + ctx, "media_player", "volume_up", + nil, EntityTarget(entityID), + ) } diff --git a/internal/services/notify.go b/internal/services/notify.go index 88dbb2a..893ea12 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -1,16 +1,18 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) type Notify struct { - conn *websocket.Conn + service Service } -func NewNotify(conn *websocket.Conn) *Notify { +func NewNotify(service Service) *Notify { return &Notify{ - conn: conn, + service: service, } } @@ -23,7 +25,8 @@ type NotifyRequest struct { } // Send a notification. -func (ha *Notify) Notify(reqData NotifyRequest) { +func (ha *Notify) Notify(reqData NotifyRequest) (websocket.Message, error) { + ctx := context.TODO() serviceData := map[string]any{ "message": reqData.Message, "title": reqData.Title, @@ -32,14 +35,8 @@ func (ha *Notify) Notify(reqData NotifyRequest) { serviceData["data"] = reqData.Data } - req := CallServiceRequest{ - Domain: "notify", - Service: reqData.ServiceName, - ServiceData: serviceData, - } - - ha.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) + return ha.service.CallService( + ctx, "notify", reqData.ServiceName, + serviceData, Target{}, + ) } diff --git a/internal/services/number.go b/internal/services/number.go index 7ccf458..e5744ce 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -1,35 +1,30 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Number struct { - conn *websocket.Conn + service Service } -func NewNumber(conn *websocket.Conn) *Number { +func NewNumber(service Service) *Number { return &Number{ - conn: conn, + service: service, } } /* Public API */ -func (ib Number) SetValue(entityID string, value float32) { - req := CallServiceRequest{ - Domain: "number", - Service: "set_value", - Target: Target{ - EntityID: entityID, - }, - ServiceData: map[string]any{"value": value}, - } - - ib.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (ib Number) SetValue(entityID string, value float32) (websocket.Message, error) { + ctx := context.TODO() + return ib.service.CallService( + ctx, "number", "set_value", + map[string]any{"value": value}, + EntityTarget(entityID), + ) } diff --git a/internal/services/scene.go b/internal/services/scene.go index 03f5671..de2c45f 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -1,80 +1,56 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Scene struct { - conn *websocket.Conn + service Service } -func NewScene(conn *websocket.Conn) *Scene { +func NewScene(service Service) *Scene { return &Scene{ - conn: conn, + service: service, } } /* Public API */ // Apply a scene. Takes map that is translated into service_data. -func (s Scene) Apply(serviceData any) { - req := CallServiceRequest{ - Domain: "scene", - Service: "apply", - ServiceData: serviceData, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Scene) Apply(serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "scene", "apply", + serviceData, Target{}, + ) } // Create a scene entity. -func (s Scene) Create(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "scene", - Service: "create", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Scene) Create(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "scene", "create", + serviceData, EntityTarget(entityID), + ) } // Reload the scenes. -func (s Scene) Reload() { - req := CallServiceRequest{ - Domain: "scene", - Service: "reload", - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Scene) Reload() (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "scene", "reload", nil, Target{}, + ) } // TurnOn a scene entity. -func (s Scene) TurnOn(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "scene", - Service: "turn_on", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Scene) TurnOn(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "scene", "turn_on", + serviceData, EntityTarget(entityID), + ) } diff --git a/internal/services/script.go b/internal/services/script.go index 467d4be..f68334c 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -1,80 +1,57 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Script struct { - conn *websocket.Conn + service Service } -func NewScript(conn *websocket.Conn) *Script { +func NewScript(service Service) *Script { return &Script{ - conn: conn, + service: service, } } /* Public API */ // Reload a script that was created in the HA UI. -func (s Script) Reload(entityID string) { - req := CallServiceRequest{ - Domain: "script", - Service: "reload", - Target: Target{ - EntityID: entityID, - }, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Script) Reload(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "script", "reload", + nil, EntityTarget(entityID), + ) } // Toggle a script that was created in the HA UI. -func (s Script) Toggle(entityID string) { - req := CallServiceRequest{ - Domain: "script", - Service: "toggle", - Target: Target{ - EntityID: entityID, - }, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Script) Toggle(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "script", "toggle", + nil, EntityTarget(entityID), + ) } // Turn off a script that was created in the HA UI. -func (s Script) TurnOff() { - req := CallServiceRequest{ - Domain: "script", - Service: "turn_off", - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Script) TurnOff() (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "script", "turn_off", + nil, Target{}, + ) } // Turn on a script that was created in the HA UI. -func (s Script) TurnOn(entityID string) { - req := CallServiceRequest{ - Domain: "script", - Service: "turn_on", - Target: Target{ - EntityID: entityID, - }, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Script) TurnOn(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "script", "turn_on", + nil, EntityTarget(entityID), + ) } diff --git a/internal/services/services.go b/internal/services/services.go index 21016e7..c495f2f 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -26,26 +26,3 @@ type Service interface { ctx context.Context, domain string, service string, serviceData any, target Target, ) (websocket.Message, error) } - -// CallService is a type that always serializes as `"call_service"`. -type CallService struct{} - -func (CallService) String() string { - return "call_service" -} - -func (CallService) MarshalJSON() ([]byte, error) { - return []byte(`"call_service"`), nil -} - -type CallServiceRequest struct { - ID int64 `json:"id"` - RequestType CallService `json:"type"` // hardcoded "call_service" - Domain string `json:"domain"` - Service string `json:"service"` - - // ServiceData must be serializable to a JSON object. - ServiceData any `json:"service_data,omitempty"` - - Target Target `json:"target,omitempty"` -} diff --git a/internal/services/switch.go b/internal/services/switch.go index 9176f28..83a0483 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -1,64 +1,45 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Switch struct { - conn *websocket.Conn + service Service } -func NewSwitch(conn *websocket.Conn) *Switch { +func NewSwitch(service Service) *Switch { return &Switch{ - conn: conn, + service: service, } } /* Public API */ -func (s Switch) TurnOn(entityID string) { - req := CallServiceRequest{ - Domain: "switch", - Service: "turn_on", - Target: Target{ - EntityID: entityID, - }, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Switch) TurnOn(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "switch", "turn_on", + nil, EntityTarget(entityID), + ) } -func (s Switch) Toggle(entityID string) { - req := CallServiceRequest{ - Domain: "switch", - Service: "toggle", - Target: Target{ - EntityID: entityID, - }, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Switch) Toggle(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "switch", "toggle", + nil, EntityTarget(entityID), + ) } -func (s Switch) TurnOff(entityID string) { - req := CallServiceRequest{ - Domain: "switch", - Service: "turn_off", - Target: Target{ - EntityID: entityID, - }, - } - - s.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (s Switch) TurnOff(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return s.service.CallService( + ctx, "switch", "turn_off", + nil, EntityTarget(entityID), + ) } diff --git a/internal/services/tts.go b/internal/services/tts.go index 29c599f..2faf0e7 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -1,67 +1,48 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type TTS struct { - conn *websocket.Conn + service Service } -func NewTTS(conn *websocket.Conn) *TTS { +func NewTTS(service Service) *TTS { return &TTS{ - conn: conn, + service: service, } } /* Public API */ // Remove all text-to-speech cache files and RAM cache. -func (tts TTS) ClearCache() { - req := CallServiceRequest{ - Domain: "tts", - Service: "clear_cache", - } - - tts.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (tts TTS) ClearCache() (websocket.Message, error) { + ctx := context.TODO() + return tts.service.CallService( + ctx, "tts", "clear_cache", nil, Target{}, + ) } // Say something using text-to-speech on a media player with cloud. -func (tts TTS) CloudSay(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "tts", - Service: "cloud_say", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - tts.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (tts TTS) CloudSay(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return tts.service.CallService( + ctx, "tts", "cloud_say", + serviceData, EntityTarget(entityID), + ) } // Say something using text-to-speech on a media player with // google_translate. -func (tts TTS) GoogleTranslateSay(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "tts", - Service: "google_translate_say", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - tts.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (tts TTS) GoogleTranslateSay(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return tts.service.CallService( + ctx, "tts", "google_translate_say", + serviceData, EntityTarget(entityID), + ) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 32b989c..13db03f 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -1,197 +1,120 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type Vacuum struct { - conn *websocket.Conn + service Service } -func NewVacuum(conn *websocket.Conn) *Vacuum { +func NewVacuum(service Service) *Vacuum { return &Vacuum{ - conn: conn, + service: service, } } /* Public API */ // Tell the vacuum cleaner to do a spot clean-up. -func (v Vacuum) CleanSpot(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "clean_spot", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) CleanSpot(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "clean_spot", + nil, EntityTarget(entityID), + ) } // Locate the vacuum cleaner robot. -func (v Vacuum) Locate(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "locate", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) Locate(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "locate", + nil, EntityTarget(entityID), + ) } // Pause the cleaning task. -func (v Vacuum) Pause(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "pause", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) Pause(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "pause", + nil, EntityTarget(entityID), + ) } // Tell the vacuum cleaner to return to its dock. -func (v Vacuum) ReturnToBase(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "return_to_base", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) ReturnToBase(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "return_to_base", + nil, EntityTarget(entityID), + ) } // Send a raw command to the vacuum cleaner. -func (v Vacuum) SendCommand(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "send_command", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) SendCommand(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "send_command", + serviceData, EntityTarget(entityID), + ) } // Set the fan speed of the vacuum cleaner. -func (v Vacuum) SetFanSpeed(entityID string, serviceData any) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "set_fan_speed", - Target: Target{ - EntityID: entityID, - }, - ServiceData: serviceData, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) SetFanSpeed(entityID string, serviceData any) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "set_fan_speed", + serviceData, EntityTarget(entityID), + ) } // Start or resume the cleaning task. -func (v Vacuum) Start(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "start", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) Start(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "start", + nil, EntityTarget(entityID), + ) } // Start, pause, or resume the cleaning task. -func (v Vacuum) StartPause(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "start_pause", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) StartPause(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "start_pause", + nil, EntityTarget(entityID), + ) } // Stop the current cleaning task. -func (v Vacuum) Stop(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "stop", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) Stop(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "stop", + nil, EntityTarget(entityID), + ) } // Stop the current cleaning task and return to home. -func (v Vacuum) TurnOff(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "turn_off", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) TurnOff(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "turn_off", + nil, EntityTarget(entityID), + ) } // Start a new cleaning task. -func (v Vacuum) TurnOn(entityID string) { - req := CallServiceRequest{ - Domain: "vacuum", - Service: "turn_on", - Target: Target{ - EntityID: entityID, - }, - } - - v.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) +func (v Vacuum) TurnOn(entityID string) (websocket.Message, error) { + ctx := context.TODO() + return v.service.CallService( + ctx, "vacuum", "turn_on", + nil, EntityTarget(entityID), + ) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index c0fe45b..1269a75 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -1,39 +1,36 @@ package services import ( + "context" + "saml.dev/gome-assistant/internal/websocket" ) /* Structs */ type ZWaveJS struct { - conn *websocket.Conn + service Service } -func NewZWaveJS(conn *websocket.Conn) *ZWaveJS { +func NewZWaveJS(service Service) *ZWaveJS { return &ZWaveJS{ - conn: conn, + service: service, } } /* Public API */ // ZWaveJS bulk_set_partial_config_parameters service. -func (zw ZWaveJS) BulkSetPartialConfigParam(entityID string, parameter int, value any) { - req := CallServiceRequest{ - Domain: "zwave_js", - Service: "bulk_set_partial_config_parameters", - Target: Target{ - EntityID: entityID, - }, - ServiceData: map[string]any{ +func (zw ZWaveJS) BulkSetPartialConfigParam( + entityID string, parameter int, value any, +) (websocket.Message, error) { + ctx := context.TODO() + return zw.service.CallService( + ctx, "zwave_js", "bulk_set_partial_config_parameters", + map[string]any{ "parameter": parameter, "value": value, }, - } - - zw.conn.Send(func(lc websocket.LockedConn) error { - req.ID = lc.NextID() - return lc.SendMessage(req) - }) + EntityTarget(entityID), + ) } diff --git a/service.go b/service.go index d473047..50ee28d 100644 --- a/service.go +++ b/service.go @@ -30,28 +30,27 @@ type Service struct { } func newService(app *App, httpClient *http.HttpClient) *Service { - conn := app.wsConn return &Service{ - AlarmControlPanel: services.NewAlarmControlPanel(conn), - Climate: services.NewClimate(conn), - Cover: services.NewCover(conn), + AlarmControlPanel: services.NewAlarmControlPanel(app), + Climate: services.NewClimate(app), + Cover: services.NewCover(app), Light: services.NewLight(app), - HomeAssistant: services.NewHomeAssistant(conn), - Lock: services.NewLock(conn), - MediaPlayer: services.NewMediaPlayer(conn), - Switch: services.NewSwitch(conn), - InputBoolean: services.NewInputBoolean(conn), - InputButton: services.NewInputButton(conn), - InputText: services.NewInputText(conn), - InputDatetime: services.NewInputDatetime(conn), - InputNumber: services.NewInputNumber(conn), + HomeAssistant: services.NewHomeAssistant(app), + Lock: services.NewLock(app), + MediaPlayer: services.NewMediaPlayer(app), + Switch: services.NewSwitch(app), + InputBoolean: services.NewInputBoolean(app), + InputButton: services.NewInputButton(app), + InputText: services.NewInputText(app), + InputDatetime: services.NewInputDatetime(app), + InputNumber: services.NewInputNumber(app), Event: services.NewEvent(app), - Notify: services.NewNotify(conn), - Number: services.NewNumber(conn), - Scene: services.NewScene(conn), - Script: services.NewScript(conn), - TTS: services.NewTTS(conn), - Vacuum: services.NewVacuum(conn), - ZWaveJS: services.NewZWaveJS(conn), + Notify: services.NewNotify(app), + Number: services.NewNumber(app), + Scene: services.NewScene(app), + Script: services.NewScript(app), + TTS: services.NewTTS(app), + Vacuum: services.NewVacuum(app), + ZWaveJS: services.NewZWaveJS(app), } } From 10c8d53501f30e9404833eeddfac1e460d738ac7 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 21 Apr 2024 23:19:18 +0200 Subject: [PATCH 060/103] Make the websocket package public --- app.go | 2 +- entitylistener.go | 2 +- eventListener.go | 2 +- internal/services/alarm_control_panel.go | 2 +- internal/services/climate.go | 2 +- internal/services/cover.go | 2 +- internal/services/event.go | 2 +- internal/services/homeassistant.go | 2 +- internal/services/input_boolean.go | 2 +- internal/services/input_button.go | 2 +- internal/services/input_datetime.go | 2 +- internal/services/input_number.go | 2 +- internal/services/input_text.go | 2 +- internal/services/light.go | 2 +- internal/services/lock.go | 2 +- internal/services/media_player.go | 2 +- internal/services/notify.go | 2 +- internal/services/number.go | 2 +- internal/services/scene.go | 2 +- internal/services/script.go | 2 +- internal/services/services.go | 2 +- internal/services/switch.go | 2 +- internal/services/tts.go | 2 +- internal/services/vacuum.go | 2 +- internal/services/zwavejs.go | 2 +- {internal/websocket => websocket}/locked_conn.go | 0 {internal/websocket => websocket}/message.go | 0 {internal/websocket => websocket}/read.go | 0 {internal/websocket => websocket}/send.go | 0 {internal/websocket => websocket}/subscriptions.go | 0 {internal/websocket => websocket}/websocket.go | 0 31 files changed, 25 insertions(+), 25 deletions(-) rename {internal/websocket => websocket}/locked_conn.go (100%) rename {internal/websocket => websocket}/message.go (100%) rename {internal/websocket => websocket}/read.go (100%) rename {internal/websocket => websocket}/send.go (100%) rename {internal/websocket => websocket}/subscriptions.go (100%) rename {internal/websocket => websocket}/websocket.go (100%) diff --git a/app.go b/app.go index 8b851ff..3147a15 100644 --- a/app.go +++ b/app.go @@ -15,7 +15,7 @@ import ( "saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/priorityqueue" "saml.dev/gome-assistant/internal/services" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) // Returned by NewApp() if authentication fails diff --git a/entitylistener.go b/entitylistener.go index 6ecee6c..11aa5fa 100644 --- a/entitylistener.go +++ b/entitylistener.go @@ -7,7 +7,7 @@ import ( "github.com/golang-module/carbon" "saml.dev/gome-assistant/internal" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) type EntityListener struct { diff --git a/eventListener.go b/eventListener.go index 8c1ccbc..b4aae27 100644 --- a/eventListener.go +++ b/eventListener.go @@ -8,7 +8,7 @@ import ( "github.com/golang-module/carbon" "saml.dev/gome-assistant/internal" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) type EventListener struct { diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 1820080..bf7c37b 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/climate.go b/internal/services/climate.go index 2adf45d..65cbd88 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/cover.go b/internal/services/cover.go index 1da3267..c6a1d35 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/event.go b/internal/services/event.go index 483ecc2..ead6c42 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) type Event struct { diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 8e75669..08d7893 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) type HomeAssistant struct { diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 92beb22..ee7f1fd 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 69a57a6..864382f 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 6c8f1b5..ac6a679 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 73e25f5..7a1bc0f 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/input_text.go b/internal/services/input_text.go index a1bb377..57aad94 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/light.go b/internal/services/light.go index bd7e5d6..456cc37 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/lock.go b/internal/services/lock.go index e15a3ba..b4c4727 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/media_player.go b/internal/services/media_player.go index ffc512f..e424688 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/notify.go b/internal/services/notify.go index 893ea12..2a56e7c 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) type Notify struct { diff --git a/internal/services/number.go b/internal/services/number.go index e5744ce..7057bec 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/scene.go b/internal/services/scene.go index de2c45f..47b02ef 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/script.go b/internal/services/script.go index f68334c..f2e407f 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/services.go b/internal/services/services.go index c495f2f..bbc13cb 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) // Target represents the target of the service call, if applicable. diff --git a/internal/services/switch.go b/internal/services/switch.go index 83a0483..9e22e4c 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/tts.go b/internal/services/tts.go index 2faf0e7..d76b1a5 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 13db03f..2c405b1 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 1269a75..180a526 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -3,7 +3,7 @@ package services import ( "context" - "saml.dev/gome-assistant/internal/websocket" + "saml.dev/gome-assistant/websocket" ) /* Structs */ diff --git a/internal/websocket/locked_conn.go b/websocket/locked_conn.go similarity index 100% rename from internal/websocket/locked_conn.go rename to websocket/locked_conn.go diff --git a/internal/websocket/message.go b/websocket/message.go similarity index 100% rename from internal/websocket/message.go rename to websocket/message.go diff --git a/internal/websocket/read.go b/websocket/read.go similarity index 100% rename from internal/websocket/read.go rename to websocket/read.go diff --git a/internal/websocket/send.go b/websocket/send.go similarity index 100% rename from internal/websocket/send.go rename to websocket/send.go diff --git a/internal/websocket/subscriptions.go b/websocket/subscriptions.go similarity index 100% rename from internal/websocket/subscriptions.go rename to websocket/subscriptions.go diff --git a/internal/websocket/websocket.go b/websocket/websocket.go similarity index 100% rename from internal/websocket/websocket.go rename to websocket/websocket.go From f0699880d05136f7e757a1e79c76a133d7e023c9 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 15:28:12 +0200 Subject: [PATCH 061/103] Target: allow device targets --- internal/services/services.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/services/services.go b/internal/services/services.go index bbc13cb..c739292 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -9,6 +9,7 @@ import ( // Target represents the target of the service call, if applicable. type Target struct { EntityID string `json:"entity_id,omitempty"` + DeviceID string `json:"device_id,omitempty"` } func EntityTarget(entityID string) Target { @@ -17,6 +18,12 @@ func EntityTarget(entityID string) Target { } } +func DeviceTarget(deviceID string) Target { + return Target{ + DeviceID: deviceID, + } +} + type Service interface { Call( ctx context.Context, req websocket.Request, From 826704b78c3572acc3219bed09fc10a0e7fd998e Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 15:33:45 +0200 Subject: [PATCH 062/103] Move the "application" code to package `app` --- app.go => app/app.go | 2 +- checkers.go => app/checkers.go | 2 +- checkers_test.go => app/checkers_test.go | 2 +- entitylistener.go => app/entitylistener.go | 2 +- eventListener.go => app/eventListener.go | 2 +- eventTypes.go => app/eventTypes.go | 2 +- interval.go => app/interval.go | 2 +- schedule.go => app/schedule.go | 2 +- service.go => app/service.go | 2 +- state.go => app/state.go | 2 +- example/example.go | 24 +++++++++++----------- example/example_live_test.go | 17 +++++++-------- 12 files changed, 31 insertions(+), 30 deletions(-) rename app.go => app/app.go (99%) rename checkers.go => app/checkers.go (99%) rename checkers_test.go => app/checkers_test.go (99%) rename entitylistener.go => app/entitylistener.go (99%) rename eventListener.go => app/eventListener.go (99%) rename eventTypes.go => app/eventTypes.go (97%) rename interval.go => app/interval.go (99%) rename schedule.go => app/schedule.go (99%) rename service.go => app/service.go (98%) rename state.go => app/state.go (99%) diff --git a/app.go b/app/app.go similarity index 99% rename from app.go rename to app/app.go index 3147a15..cabcfa3 100644 --- a/app.go +++ b/app/app.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "context" diff --git a/checkers.go b/app/checkers.go similarity index 99% rename from checkers.go rename to app/checkers.go index 029f10e..7b80a0a 100644 --- a/checkers.go +++ b/app/checkers.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "time" diff --git a/checkers_test.go b/app/checkers_test.go similarity index 99% rename from checkers_test.go rename to app/checkers_test.go index 22dd451..9204528 100644 --- a/checkers_test.go +++ b/app/checkers_test.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "errors" diff --git a/entitylistener.go b/app/entitylistener.go similarity index 99% rename from entitylistener.go rename to app/entitylistener.go index 11aa5fa..acde43b 100644 --- a/entitylistener.go +++ b/app/entitylistener.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "encoding/json" diff --git a/eventListener.go b/app/eventListener.go similarity index 99% rename from eventListener.go rename to app/eventListener.go index b4aae27..febab01 100644 --- a/eventListener.go +++ b/app/eventListener.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "encoding/json" diff --git a/eventTypes.go b/app/eventTypes.go similarity index 97% rename from eventTypes.go rename to app/eventTypes.go index cefc6dc..4dfbefb 100644 --- a/eventTypes.go +++ b/app/eventTypes.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import "time" diff --git a/interval.go b/app/interval.go similarity index 99% rename from interval.go rename to app/interval.go index 07467c6..55d3dc2 100644 --- a/interval.go +++ b/app/interval.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "fmt" diff --git a/schedule.go b/app/schedule.go similarity index 99% rename from schedule.go rename to app/schedule.go index cf6eb37..085b582 100644 --- a/schedule.go +++ b/app/schedule.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "fmt" diff --git a/service.go b/app/service.go similarity index 98% rename from service.go rename to app/service.go index 50ee28d..b41eb13 100644 --- a/service.go +++ b/app/service.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "saml.dev/gome-assistant/internal/http" diff --git a/state.go b/app/state.go similarity index 99% rename from state.go rename to app/state.go index 858988c..e7e3370 100644 --- a/state.go +++ b/app/state.go @@ -1,4 +1,4 @@ -package gomeassistant +package app import ( "encoding/json" diff --git a/example/example.go b/example/example.go index 3975cd4..6015859 100644 --- a/example/example.go +++ b/example/example.go @@ -7,14 +7,14 @@ import ( "os" "time" - ga "saml.dev/gome-assistant" + gaapp "saml.dev/gome-assistant/app" ) func main() { ctx := context.Background() - app, err := ga.NewApp( + app, err := gaapp.NewApp( ctx, - ga.NewAppRequest{ + gaapp.NewAppRequest{ IpAddress: "192.168.86.67", // Replace with your Home Assistant IP Address HAAuthToken: os.Getenv("HA_AUTH_TOKEN"), HomeZoneEntityID: "zone.home", @@ -27,25 +27,25 @@ func main() { defer app.Close() - pantryDoor := ga. + pantryDoor := gaapp. NewEntityListener(). EntityIDs("binary_sensor.pantry_door"). Call(pantryLights). Build() - _11pmSched := ga. + _11pmSched := gaapp. NewDailySchedule(). Call(lightsOut). At("23:00"). Build() - _30minsBeforeSunrise := ga. + _30minsBeforeSunrise := gaapp. NewDailySchedule(). Call(sunriseSched). Sunrise("-30m"). Build() - zwaveEventListener := ga. + zwaveEventListener := gaapp. NewEventListener(). EventTypes("zwave_js_value_notification"). Call(onEvent). @@ -58,7 +58,7 @@ func main() { app.Start(ctx) } -func pantryLights(service *ga.Service, state ga.State, sensor ga.EntityData) { +func pantryLights(service *gaapp.Service, state gaapp.State, sensor gaapp.EntityData) { l := "light.pantry" if sensor.ToState == "on" { service.HomeAssistant.TurnOn(l, nil) @@ -67,18 +67,18 @@ func pantryLights(service *ga.Service, state ga.State, sensor ga.EntityData) { } } -func onEvent(service *ga.Service, state ga.State, data ga.EventData) { +func onEvent(service *gaapp.Service, state gaapp.State, data gaapp.EventData) { // Since the structure of the event changes depending // on the event type, you can Unmarshal the raw json // into a Go type. If a type for your event doesn't // exist, you can write it yourself! PR's welcome to // the eventTypes.go file :) - ev := ga.EventZWaveJSValueNotification{} + ev := gaapp.EventZWaveJSValueNotification{} json.Unmarshal(data.RawEventJSON, &ev) slog.Info("On event invoked", "event", ev) } -func lightsOut(service *ga.Service, state ga.State) { +func lightsOut(service *gaapp.Service, state gaapp.State) { // always turn off outside lights service.Light.TurnOff("light.outside_lights") s, err := state.Get("binary_sensor.living_room_motion") @@ -93,7 +93,7 @@ func lightsOut(service *ga.Service, state ga.State) { } } -func sunriseSched(service *ga.Service, state ga.State) { +func sunriseSched(service *gaapp.Service, state gaapp.State) { service.Light.TurnOn("light.living_room_lamps", nil) service.Light.TurnOff("light.christmas_lights") } diff --git a/example/example_live_test.go b/example/example_live_test.go index 40d914c..66166cf 100644 --- a/example/example_live_test.go +++ b/example/example_live_test.go @@ -11,13 +11,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gopkg.in/yaml.v3" - ga "saml.dev/gome-assistant" + + gaapp "saml.dev/gome-assistant/app" ) type ( MySuite struct { suite.Suite - app *ga.App + app *gaapp.App config *Config suiteCtx map[string]any } @@ -62,9 +63,9 @@ func (s *MySuite) SetupSuite(ctx context.Context) { slog.Error("Error unmarshalling config file", err) } - s.app, err = ga.NewApp( + s.app, err = gaapp.NewApp( ctx, - ga.NewAppRequest{ + gaapp.NewAppRequest{ HAAuthToken: s.config.Hass.HAAuthToken, IpAddress: s.config.Hass.IpAddress, HomeZoneEntityID: s.config.Hass.HomeZoneEntityID, @@ -79,13 +80,13 @@ func (s *MySuite) SetupSuite(ctx context.Context) { entityID := s.config.Entities.LightEntityID if entityID != "" { s.suiteCtx["entityCallbackInvoked"] = false - etl := ga.NewEntityListener().EntityIDs(entityID).Call(s.entityCallback).Build() + etl := gaapp.NewEntityListener().EntityIDs(entityID).Call(s.entityCallback).Build() s.app.RegisterEntityListeners(etl) } s.suiteCtx["dailyScheduleCallbackInvoked"] = false runTime := time.Now().Add(1 * time.Minute).Format("15:04") - dailySchedule := ga.NewDailySchedule().Call(s.dailyScheduleCallback).At(runTime).Build() + dailySchedule := gaapp.NewDailySchedule().Call(s.dailyScheduleCallback).At(runTime).Build() s.app.RegisterSchedules(dailySchedule) // start GA app @@ -130,7 +131,7 @@ func (s *MySuite) TestSchedule() { } // Capture event after light entity state has changed -func (s *MySuite) entityCallback(se *ga.Service, st ga.State, e ga.EntityData) { +func (s *MySuite) entityCallback(se *gaapp.Service, st gaapp.State, e gaapp.EntityData) { slog.Info( "Entity callback called.", "entity id", e.TriggerEntityID, @@ -141,7 +142,7 @@ func (s *MySuite) entityCallback(se *ga.Service, st ga.State, e ga.EntityData) { } // Capture planned daily schedule -func (s *MySuite) dailyScheduleCallback(se *ga.Service, st ga.State) { +func (s *MySuite) dailyScheduleCallback(se *gaapp.Service, st gaapp.State) { slog.Info("Daily schedule callback called.") s.suiteCtx["dailyScheduleCallbackInvoked"] = true } From 2ffa0f90150f55c6f9f106cc4e755588ba6bd3e0 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 15:50:52 +0200 Subject: [PATCH 063/103] Move `Target` to the top level --- app/app.go | 6 ++-- internal/services/alarm_control_panel.go | 15 ++++---- internal/services/climate.go | 5 +-- internal/services/cover.go | 21 +++++------ internal/services/homeassistant.go | 7 ++-- internal/services/input_boolean.go | 9 ++--- internal/services/input_button.go | 5 +-- internal/services/input_datetime.go | 5 +-- internal/services/input_number.go | 9 ++--- internal/services/input_text.go | 5 +-- internal/services/light.go | 7 ++-- internal/services/lock.go | 5 +-- internal/services/media_player.go | 45 ++++++++++++------------ internal/services/notify.go | 3 +- internal/services/number.go | 3 +- internal/services/scene.go | 9 ++--- internal/services/script.go | 9 ++--- internal/services/services.go | 21 ++--------- internal/services/switch.go | 7 ++-- internal/services/tts.go | 7 ++-- internal/services/vacuum.go | 23 ++++++------ internal/services/zwavejs.go | 3 +- target.go | 19 ++++++++++ 23 files changed, 135 insertions(+), 113 deletions(-) create mode 100644 target.go diff --git a/app/app.go b/app/app.go index cabcfa3..cc3f010 100644 --- a/app/app.go +++ b/app/app.go @@ -12,9 +12,9 @@ import ( sunriseLib "github.com/nathan-osman/go-sunrise" "golang.org/x/sync/errgroup" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/priorityqueue" - "saml.dev/gome-assistant/internal/services" "saml.dev/gome-assistant/websocket" ) @@ -580,7 +580,7 @@ type CallServiceRequest struct { // ServiceData must be serializable to a JSON object. ServiceData any `json:"service_data,omitempty"` - Target services.Target `json:"target,omitempty"` + Target ga.Target `json:"target,omitempty"` } // CallService invokes a service using a `call_service` message, then @@ -588,7 +588,7 @@ type CallServiceRequest struct { // // FIXME: can the response be parsed into a result-style message? func (app *App) CallService( - ctx context.Context, domain string, service string, serviceData any, target services.Target, + ctx context.Context, domain string, service string, serviceData any, target ga.Target, ) (websocket.Message, error) { req := CallServiceRequest{ BaseMessage: websocket.BaseMessage{ diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index bf7c37b..d781e66 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -27,7 +28,7 @@ func (acp AlarmControlPanel) ArmAway( ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_away", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -40,7 +41,7 @@ func (acp AlarmControlPanel) ArmWithCustomBypass( ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_custom_bypass", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -53,7 +54,7 @@ func (acp AlarmControlPanel) ArmHome( ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_home", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -64,7 +65,7 @@ func (acp AlarmControlPanel) ArmNight( ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_night", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -75,7 +76,7 @@ func (acp AlarmControlPanel) ArmVacation( ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_vacation", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -86,7 +87,7 @@ func (acp AlarmControlPanel) Disarm( ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_disarm", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -97,6 +98,6 @@ func (acp AlarmControlPanel) Trigger( ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_trigger", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } diff --git a/internal/services/climate.go b/internal/services/climate.go index 65cbd88..cb96f84 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,7 +26,7 @@ func (c Climate) SetFanMode( return c.service.CallService( ctx, "climate", "set_fan_mode", map[string]any{"fan_mode": fanMode}, - EntityTarget(entityID), + ga.EntityTarget(entityID), ) } @@ -60,6 +61,6 @@ func (c Climate) SetTemperature( return c.service.CallService( ctx, "climate", "set_temperature", setTemperatureRequest.ToJSON(), - EntityTarget(entityID), + ga.EntityTarget(entityID), ) } diff --git a/internal/services/cover.go b/internal/services/cover.go index c6a1d35..927f2cb 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,7 +26,7 @@ func (c Cover) Close(entityID string) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "close_cover", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -34,7 +35,7 @@ func (c Cover) CloseTilt(entityID string) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "close_cover_tilt", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -43,7 +44,7 @@ func (c Cover) Open(entityID string) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "open_cover", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -52,7 +53,7 @@ func (c Cover) OpenTilt(entityID string) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "open_cover_tilt", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -62,7 +63,7 @@ func (c Cover) SetPosition(entityID string, serviceData any) (websocket.Message, ctx := context.TODO() return c.service.CallService( ctx, "cover", "set_cover_position", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -72,7 +73,7 @@ func (c Cover) SetTiltPosition(entityID string, serviceData any) (websocket.Mess ctx := context.TODO() return c.service.CallService( ctx, "cover", "set_cover_tilt_position", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -81,7 +82,7 @@ func (c Cover) Stop(entityID string) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "stop_cover", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -90,7 +91,7 @@ func (c Cover) StopTilt(entityID string) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "stop_cover_tilt", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -99,7 +100,7 @@ func (c Cover) Toggle(entityID string) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "toggle", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -108,6 +109,6 @@ func (c Cover) ToggleTilt(entityID string) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "toggle_cover_tilt", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 08d7893..71e82db 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -22,7 +23,7 @@ func (ha *HomeAssistant) TurnOn(entityID string, serviceData any) (websocket.Mes ctx := context.TODO() return ha.service.CallService( ctx, "homeassistant", "turn_on", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -32,7 +33,7 @@ func (ha *HomeAssistant) Toggle(entityID string, serviceData any) (websocket.Mes ctx := context.TODO() return ha.service.CallService( ctx, "homeassistant", "toggle", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -40,6 +41,6 @@ func (ha *HomeAssistant) TurnOff(entityID string) (websocket.Message, error) { ctx := context.TODO() return ha.service.CallService( ctx, "homeassistant", "turn_off", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index ee7f1fd..e9bb763 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -24,7 +25,7 @@ func (ib InputBoolean) TurnOn(entityID string) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_boolean", "turn_on", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -32,7 +33,7 @@ func (ib InputBoolean) Toggle(entityID string) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_boolean", "toggle", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -40,13 +41,13 @@ func (ib InputBoolean) TurnOff(entityID string) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_boolean", "turn_off", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } func (ib InputBoolean) Reload() (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( - ctx, "input_boolean", "reload", nil, Target{}, + ctx, "input_boolean", "reload", nil, ga.Target{}, ) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 864382f..1c6de9a 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -24,13 +25,13 @@ func (ib InputButton) Press(entityID string) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_button", "press", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } func (ib InputButton) Reload() (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( - ctx, "input_button", "reload", nil, Target{}, + ctx, "input_button", "reload", nil, ga.Target{}, ) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index ac6a679..2a064f1 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -29,13 +30,13 @@ func (ib InputDatetime) Set(entityID string, value time.Time) (websocket.Message map[string]any{ "timestamp": fmt.Sprint(value.Unix()), }, - EntityTarget(entityID), + ga.EntityTarget(entityID), ) } func (ib InputDatetime) Reload() (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( - ctx, "input_datetime", "reload", nil, Target{}, + ctx, "input_datetime", "reload", nil, ga.Target{}, ) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 7a1bc0f..1f14b00 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,7 +26,7 @@ func (ib InputNumber) Set(entityID string, value float32) (websocket.Message, er return ib.service.CallService( ctx, "input_number", "set_value", map[string]any{"value": value}, - EntityTarget(entityID), + ga.EntityTarget(entityID), ) } @@ -33,7 +34,7 @@ func (ib InputNumber) Increment(entityID string) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_number", "increment", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -41,13 +42,13 @@ func (ib InputNumber) Decrement(entityID string) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_number", "decrement", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } func (ib InputNumber) Reload() (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( - ctx, "input_number", "reload", nil, Target{}, + ctx, "input_number", "reload", nil, ga.Target{}, ) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 57aad94..0153896 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -27,13 +28,13 @@ func (ib InputText) Set(entityID string, value string) (websocket.Message, error map[string]any{ "value": value, }, - EntityTarget(entityID), + ga.EntityTarget(entityID), ) } func (ib InputText) Reload() (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( - ctx, "input_text", "reload", nil, Target{}, + ctx, "input_text", "reload", nil, ga.Target{}, ) } diff --git a/internal/services/light.go b/internal/services/light.go index 456cc37..219a787 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -24,7 +25,7 @@ func NewLight(service Service) *Light { func (l Light) TurnOn(entityID string, serviceData any) (websocket.Message, error) { ctx := context.TODO() return l.service.CallService( - ctx, "light", "turn_on", serviceData, EntityTarget(entityID), + ctx, "light", "turn_on", serviceData, ga.EntityTarget(entityID), ) } @@ -32,13 +33,13 @@ func (l Light) TurnOn(entityID string, serviceData any) (websocket.Message, erro func (l Light) Toggle(entityID string, serviceData any) (websocket.Message, error) { ctx := context.TODO() return l.service.CallService( - ctx, "light", "toggle", serviceData, EntityTarget(entityID), + ctx, "light", "toggle", serviceData, ga.EntityTarget(entityID), ) } func (l Light) TurnOff(entityID string) (websocket.Message, error) { ctx := context.TODO() return l.service.CallService( - ctx, "light", "turn_off", nil, EntityTarget(entityID), + ctx, "light", "turn_off", nil, ga.EntityTarget(entityID), ) } diff --git a/internal/services/lock.go b/internal/services/lock.go index b4c4727..b1031e3 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,7 +26,7 @@ func (l Lock) Lock(entityID string, serviceData any) (websocket.Message, error) ctx := context.TODO() return l.service.CallService( ctx, "lock", "lock", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -34,6 +35,6 @@ func (l Lock) Unlock(entityID string, serviceData any) (websocket.Message, error ctx := context.TODO() return l.service.CallService( ctx, "lock", "unlock", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index e424688..7efd1c2 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,7 +26,7 @@ func (mp MediaPlayer) ClearPlaylist(entityID string) (websocket.Message, error) ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "clear_playlist", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -34,7 +35,7 @@ func (mp MediaPlayer) Join(entityID string, serviceData any) (websocket.Message, ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "join", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -43,7 +44,7 @@ func (mp MediaPlayer) Next(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_next_track", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -52,7 +53,7 @@ func (mp MediaPlayer) Pause(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_pause", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -61,7 +62,7 @@ func (mp MediaPlayer) Play(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_play", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -70,7 +71,7 @@ func (mp MediaPlayer) PlayPause(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_play_pause", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -79,7 +80,7 @@ func (mp MediaPlayer) Previous(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_previous_track", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -88,7 +89,7 @@ func (mp MediaPlayer) Seek(entityID string, serviceData any) (websocket.Message, ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_seek", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -97,7 +98,7 @@ func (mp MediaPlayer) Stop(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_stop", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -106,7 +107,7 @@ func (mp MediaPlayer) PlayMedia(entityID string, serviceData any) (websocket.Mes ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "play_media", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -115,7 +116,7 @@ func (mp MediaPlayer) RepeatSet(entityID string, serviceData any) (websocket.Mes ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "repeat_set", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -126,7 +127,7 @@ func (mp MediaPlayer) SelectSoundMode( ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "select_sound_mode", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -137,7 +138,7 @@ func (mp MediaPlayer) SelectSource( ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "select_source", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -146,7 +147,7 @@ func (mp MediaPlayer) Shuffle(entityID string, serviceData any) (websocket.Messa ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "shuffle_set", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -155,7 +156,7 @@ func (mp MediaPlayer) Toggle(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "toggle", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -164,7 +165,7 @@ func (mp MediaPlayer) TurnOff(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "turn_off", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -173,7 +174,7 @@ func (mp MediaPlayer) TurnOn(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "turn_on", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -183,7 +184,7 @@ func (mp MediaPlayer) Unjoin(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "unjoin", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -192,7 +193,7 @@ func (mp MediaPlayer) VolumeDown(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "volume_down", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -201,7 +202,7 @@ func (mp MediaPlayer) VolumeMute(entityID string, serviceData any) (websocket.Me ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "volume_mute", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -210,7 +211,7 @@ func (mp MediaPlayer) VolumeSet(entityID string, serviceData any) (websocket.Mes ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "volume_set", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -219,6 +220,6 @@ func (mp MediaPlayer) VolumeUp(entityID string) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "volume_up", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } diff --git a/internal/services/notify.go b/internal/services/notify.go index 2a56e7c..17bc41a 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -37,6 +38,6 @@ func (ha *Notify) Notify(reqData NotifyRequest) (websocket.Message, error) { return ha.service.CallService( ctx, "notify", reqData.ServiceName, - serviceData, Target{}, + serviceData, ga.Target{}, ) } diff --git a/internal/services/number.go b/internal/services/number.go index 7057bec..e007b43 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,6 +26,6 @@ func (ib Number) SetValue(entityID string, value float32) (websocket.Message, er return ib.service.CallService( ctx, "number", "set_value", map[string]any{"value": value}, - EntityTarget(entityID), + ga.EntityTarget(entityID), ) } diff --git a/internal/services/scene.go b/internal/services/scene.go index 47b02ef..ae60457 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,7 +26,7 @@ func (s Scene) Apply(serviceData any) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "scene", "apply", - serviceData, Target{}, + serviceData, ga.Target{}, ) } @@ -34,7 +35,7 @@ func (s Scene) Create(entityID string, serviceData any) (websocket.Message, erro ctx := context.TODO() return s.service.CallService( ctx, "scene", "create", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -42,7 +43,7 @@ func (s Scene) Create(entityID string, serviceData any) (websocket.Message, erro func (s Scene) Reload() (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( - ctx, "scene", "reload", nil, Target{}, + ctx, "scene", "reload", nil, ga.Target{}, ) } @@ -51,6 +52,6 @@ func (s Scene) TurnOn(entityID string, serviceData any) (websocket.Message, erro ctx := context.TODO() return s.service.CallService( ctx, "scene", "turn_on", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } diff --git a/internal/services/script.go b/internal/services/script.go index f2e407f..78d8071 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,7 +26,7 @@ func (s Script) Reload(entityID string) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "script", "reload", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -34,7 +35,7 @@ func (s Script) Toggle(entityID string) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "script", "toggle", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -43,7 +44,7 @@ func (s Script) TurnOff() (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "script", "turn_off", - nil, Target{}, + nil, ga.Target{}, ) } @@ -52,6 +53,6 @@ func (s Script) TurnOn(entityID string) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "script", "turn_on", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } diff --git a/internal/services/services.go b/internal/services/services.go index c739292..8706b39 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -3,33 +3,16 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) -// Target represents the target of the service call, if applicable. -type Target struct { - EntityID string `json:"entity_id,omitempty"` - DeviceID string `json:"device_id,omitempty"` -} - -func EntityTarget(entityID string) Target { - return Target{ - EntityID: entityID, - } -} - -func DeviceTarget(deviceID string) Target { - return Target{ - DeviceID: deviceID, - } -} - type Service interface { Call( ctx context.Context, req websocket.Request, ) (websocket.Message, error) CallService( - ctx context.Context, domain string, service string, serviceData any, target Target, + ctx context.Context, domain string, service string, serviceData any, target ga.Target, ) (websocket.Message, error) } diff --git a/internal/services/switch.go b/internal/services/switch.go index 9e22e4c..3ff9566 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -24,7 +25,7 @@ func (s Switch) TurnOn(entityID string) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "switch", "turn_on", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -32,7 +33,7 @@ func (s Switch) Toggle(entityID string) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "switch", "toggle", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -40,6 +41,6 @@ func (s Switch) TurnOff(entityID string) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "switch", "turn_off", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } diff --git a/internal/services/tts.go b/internal/services/tts.go index d76b1a5..a90ec19 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -24,7 +25,7 @@ func NewTTS(service Service) *TTS { func (tts TTS) ClearCache() (websocket.Message, error) { ctx := context.TODO() return tts.service.CallService( - ctx, "tts", "clear_cache", nil, Target{}, + ctx, "tts", "clear_cache", nil, ga.Target{}, ) } @@ -33,7 +34,7 @@ func (tts TTS) CloudSay(entityID string, serviceData any) (websocket.Message, er ctx := context.TODO() return tts.service.CallService( ctx, "tts", "cloud_say", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -43,6 +44,6 @@ func (tts TTS) GoogleTranslateSay(entityID string, serviceData any) (websocket.M ctx := context.TODO() return tts.service.CallService( ctx, "tts", "google_translate_say", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 2c405b1..168ad50 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -25,7 +26,7 @@ func (v Vacuum) CleanSpot(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "clean_spot", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -34,7 +35,7 @@ func (v Vacuum) Locate(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "locate", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -43,7 +44,7 @@ func (v Vacuum) Pause(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "pause", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -52,7 +53,7 @@ func (v Vacuum) ReturnToBase(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "return_to_base", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -61,7 +62,7 @@ func (v Vacuum) SendCommand(entityID string, serviceData any) (websocket.Message ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "send_command", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -70,7 +71,7 @@ func (v Vacuum) SetFanSpeed(entityID string, serviceData any) (websocket.Message ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "set_fan_speed", - serviceData, EntityTarget(entityID), + serviceData, ga.EntityTarget(entityID), ) } @@ -79,7 +80,7 @@ func (v Vacuum) Start(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "start", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -88,7 +89,7 @@ func (v Vacuum) StartPause(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "start_pause", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -97,7 +98,7 @@ func (v Vacuum) Stop(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "stop", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -106,7 +107,7 @@ func (v Vacuum) TurnOff(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "turn_off", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } @@ -115,6 +116,6 @@ func (v Vacuum) TurnOn(entityID string) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "turn_on", - nil, EntityTarget(entityID), + nil, ga.EntityTarget(entityID), ) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index 180a526..cf1411e 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -3,6 +3,7 @@ package services import ( "context" + ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) @@ -31,6 +32,6 @@ func (zw ZWaveJS) BulkSetPartialConfigParam( "parameter": parameter, "value": value, }, - EntityTarget(entityID), + ga.EntityTarget(entityID), ) } diff --git a/target.go b/target.go new file mode 100644 index 0000000..2be15be --- /dev/null +++ b/target.go @@ -0,0 +1,19 @@ +package ga + +// Target represents the target of the service call, if applicable. +type Target struct { + EntityID string `json:"entity_id,omitempty"` + DeviceID string `json:"device_id,omitempty"` +} + +func EntityTarget(entityID string) Target { + return Target{ + EntityID: entityID, + } +} + +func DeviceTarget(deviceID string) Target { + return Target{ + DeviceID: deviceID, + } +} From 6fc5e5d64e3b31412f0fa874a94375f0193d65bc Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 16:03:00 +0200 Subject: [PATCH 064/103] Allow service methods to take any `Target` as argument --- example/example.go | 11 +-- example/example_live_test.go | 4 +- internal/services/alarm_control_panel.go | 28 ++++---- internal/services/climate.go | 8 +-- internal/services/cover.go | 40 +++++------ internal/services/homeassistant.go | 12 ++-- internal/services/input_boolean.go | 12 ++-- internal/services/input_button.go | 4 +- internal/services/input_datetime.go | 4 +- internal/services/input_number.go | 12 ++-- internal/services/input_text.go | 4 +- internal/services/light.go | 12 ++-- internal/services/lock.go | 8 +-- internal/services/media_player.go | 88 ++++++++++++------------ internal/services/number.go | 4 +- internal/services/scene.go | 8 +-- internal/services/script.go | 12 ++-- internal/services/switch.go | 12 ++-- internal/services/tts.go | 8 +-- internal/services/vacuum.go | 44 ++++++------ internal/services/zwavejs.go | 4 +- 21 files changed, 171 insertions(+), 168 deletions(-) diff --git a/example/example.go b/example/example.go index 6015859..5143390 100644 --- a/example/example.go +++ b/example/example.go @@ -7,6 +7,7 @@ import ( "os" "time" + ga "saml.dev/gome-assistant" gaapp "saml.dev/gome-assistant/app" ) @@ -59,7 +60,7 @@ func main() { } func pantryLights(service *gaapp.Service, state gaapp.State, sensor gaapp.EntityData) { - l := "light.pantry" + l := ga.EntityTarget("light.pantry") if sensor.ToState == "on" { service.HomeAssistant.TurnOn(l, nil) } else { @@ -80,7 +81,7 @@ func onEvent(service *gaapp.Service, state gaapp.State, data gaapp.EventData) { func lightsOut(service *gaapp.Service, state gaapp.State) { // always turn off outside lights - service.Light.TurnOff("light.outside_lights") + service.Light.TurnOff(ga.EntityTarget("light.outside_lights")) s, err := state.Get("binary_sensor.living_room_motion") if err != nil { slog.Warn("couldnt get living room motion state, doing nothing") @@ -89,11 +90,11 @@ func lightsOut(service *gaapp.Service, state gaapp.State) { // if no motion detected in living room for 30mins if s.State == "off" && time.Since(s.LastChanged).Minutes() > 30 { - service.Light.TurnOff("light.main_lights") + service.Light.TurnOff(ga.EntityTarget("light.main_lights")) } } func sunriseSched(service *gaapp.Service, state gaapp.State) { - service.Light.TurnOn("light.living_room_lamps", nil) - service.Light.TurnOff("light.christmas_lights") + service.Light.TurnOn(ga.EntityTarget("light.living_room_lamps"), nil) + service.Light.TurnOff(ga.EntityTarget("light.christmas_lights")) } diff --git a/example/example_live_test.go b/example/example_live_test.go index 66166cf..8eaff4a 100644 --- a/example/example_live_test.go +++ b/example/example_live_test.go @@ -12,6 +12,7 @@ import ( "github.com/stretchr/testify/suite" "gopkg.in/yaml.v3" + ga "saml.dev/gome-assistant" gaapp "saml.dev/gome-assistant/app" ) @@ -105,8 +106,9 @@ func (s *MySuite) TestLightService() { entityID := s.config.Entities.LightEntityID if entityID != "" { + target := ga.EntityTarget(entityID) initState := getEntityState(s, entityID) - s.app.GetService().Light.Toggle(entityID, nil) + s.app.GetService().Light.Toggle(target, nil) assert.EventuallyWithT( s.T(), diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index d781e66..3d7fb10 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -23,12 +23,12 @@ func NewAlarmControlPanel(service Service) *AlarmControlPanel { // Send the alarm the command for arm away. func (acp AlarmControlPanel) ArmAway( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_away", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } @@ -36,12 +36,12 @@ func (acp AlarmControlPanel) ArmAway( // Takes an entityID and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmWithCustomBypass( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_custom_bypass", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } @@ -49,55 +49,55 @@ func (acp AlarmControlPanel) ArmWithCustomBypass( // Takes an entityID and an optional // map that is translated into service_data. func (acp AlarmControlPanel) ArmHome( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_home", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Send the alarm the command for arm night. func (acp AlarmControlPanel) ArmNight( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_night", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Send the alarm the command for arm vacation. func (acp AlarmControlPanel) ArmVacation( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_vacation", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Send the alarm the command for disarm. func (acp AlarmControlPanel) Disarm( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_disarm", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Send the alarm the command for trigger. func (acp AlarmControlPanel) Trigger( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return acp.service.CallService( ctx, "alarm_control_panel", "alarm_trigger", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } diff --git a/internal/services/climate.go b/internal/services/climate.go index cb96f84..6522503 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -20,13 +20,13 @@ func NewClimate(service Service) *Climate { } func (c Climate) SetFanMode( - entityID string, fanMode string, + target ga.Target, fanMode string, ) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "climate", "set_fan_mode", map[string]any{"fan_mode": fanMode}, - ga.EntityTarget(entityID), + target, ) } @@ -55,12 +55,12 @@ func (r *SetTemperatureRequest) ToJSON() map[string]any { } func (c Climate) SetTemperature( - entityID string, setTemperatureRequest SetTemperatureRequest, + target ga.Target, setTemperatureRequest SetTemperatureRequest, ) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "climate", "set_temperature", setTemperatureRequest.ToJSON(), - ga.EntityTarget(entityID), + target, ) } diff --git a/internal/services/cover.go b/internal/services/cover.go index 927f2cb..e72e58c 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -22,93 +22,93 @@ func NewCover(service Service) *Cover { /* Public API */ // Close all or specified cover. Takes an entityID. -func (c Cover) Close(entityID string) (websocket.Message, error) { +func (c Cover) Close(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "close_cover", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Close all or specified cover tilt. Takes an entityID. -func (c Cover) CloseTilt(entityID string) (websocket.Message, error) { +func (c Cover) CloseTilt(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "close_cover_tilt", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Open all or specified cover. Takes an entityID. -func (c Cover) Open(entityID string) (websocket.Message, error) { +func (c Cover) Open(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "open_cover", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Open all or specified cover tilt. Takes an entityID. -func (c Cover) OpenTilt(entityID string) (websocket.Message, error) { +func (c Cover) OpenTilt(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "open_cover_tilt", - nil, ga.EntityTarget(entityID), + nil, target, ) } // 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 any) (websocket.Message, error) { +func (c Cover) SetPosition(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "set_cover_position", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // 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 any) (websocket.Message, error) { +func (c Cover) SetTiltPosition(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "set_cover_tilt_position", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Stop a cover entity. Takes an entityID. -func (c Cover) Stop(entityID string) (websocket.Message, error) { +func (c Cover) Stop(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "stop_cover", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Stop a cover entity tilt. Takes an entityID. -func (c Cover) StopTilt(entityID string) (websocket.Message, error) { +func (c Cover) StopTilt(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "stop_cover_tilt", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Toggle a cover open/closed. Takes an entityID. -func (c Cover) Toggle(entityID string) (websocket.Message, error) { +func (c Cover) Toggle(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "toggle", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Toggle a cover tilt open/closed. Takes an entityID. -func (c Cover) ToggleTilt(entityID string) (websocket.Message, error) { +func (c Cover) ToggleTilt(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return c.service.CallService( ctx, "cover", "toggle_cover_tilt", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 71e82db..05093e9 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -19,28 +19,28 @@ func NewHomeAssistant(service Service) *HomeAssistant { // 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 any) (websocket.Message, error) { +func (ha *HomeAssistant) TurnOn(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return ha.service.CallService( ctx, "homeassistant", "turn_on", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // 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 any) (websocket.Message, error) { +func (ha *HomeAssistant) Toggle(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return ha.service.CallService( ctx, "homeassistant", "toggle", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } -func (ha *HomeAssistant) TurnOff(entityID string) (websocket.Message, error) { +func (ha *HomeAssistant) TurnOff(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return ha.service.CallService( ctx, "homeassistant", "turn_off", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index e9bb763..2a7eadd 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -21,27 +21,27 @@ func NewInputBoolean(service Service) *InputBoolean { /* Public API */ -func (ib InputBoolean) TurnOn(entityID string) (websocket.Message, error) { +func (ib InputBoolean) TurnOn(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_boolean", "turn_on", - nil, ga.EntityTarget(entityID), + nil, target, ) } -func (ib InputBoolean) Toggle(entityID string) (websocket.Message, error) { +func (ib InputBoolean) Toggle(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_boolean", "toggle", - nil, ga.EntityTarget(entityID), + nil, target, ) } -func (ib InputBoolean) TurnOff(entityID string) (websocket.Message, error) { +func (ib InputBoolean) TurnOff(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_boolean", "turn_off", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 1c6de9a..1152953 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -21,11 +21,11 @@ func NewInputButton(service Service) *InputButton { /* Public API */ -func (ib InputButton) Press(entityID string) (websocket.Message, error) { +func (ib InputButton) Press(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_button", "press", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index 2a064f1..cf60be2 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -23,14 +23,14 @@ func NewInputDatetime(service Service) *InputDatetime { /* Public API */ -func (ib InputDatetime) Set(entityID string, value time.Time) (websocket.Message, error) { +func (ib InputDatetime) Set(target ga.Target, value time.Time) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_datetime", "set_datetime", map[string]any{ "timestamp": fmt.Sprint(value.Unix()), }, - ga.EntityTarget(entityID), + target, ) } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index 1f14b00..bd36dd9 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -21,28 +21,28 @@ func NewInputNumber(service Service) *InputNumber { /* Public API */ -func (ib InputNumber) Set(entityID string, value float32) (websocket.Message, error) { +func (ib InputNumber) Set(target ga.Target, value float32) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_number", "set_value", map[string]any{"value": value}, - ga.EntityTarget(entityID), + target, ) } -func (ib InputNumber) Increment(entityID string) (websocket.Message, error) { +func (ib InputNumber) Increment(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_number", "increment", - nil, ga.EntityTarget(entityID), + nil, target, ) } -func (ib InputNumber) Decrement(entityID string) (websocket.Message, error) { +func (ib InputNumber) Decrement(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_number", "decrement", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 0153896..8d5e832 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -21,14 +21,14 @@ func NewInputText(service Service) *InputText { /* Public API */ -func (ib InputText) Set(entityID string, value string) (websocket.Message, error) { +func (ib InputText) Set(target ga.Target, value string) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "input_text", "set_value", map[string]any{ "value": value, }, - ga.EntityTarget(entityID), + target, ) } diff --git a/internal/services/light.go b/internal/services/light.go index 219a787..cbc3bb9 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -22,24 +22,24 @@ func NewLight(service Service) *Light { /* Public API */ // TurnOn a light entity. -func (l Light) TurnOn(entityID string, serviceData any) (websocket.Message, error) { +func (l Light) TurnOn(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return l.service.CallService( - ctx, "light", "turn_on", serviceData, ga.EntityTarget(entityID), + ctx, "light", "turn_on", serviceData, target, ) } // Toggle a light entity. -func (l Light) Toggle(entityID string, serviceData any) (websocket.Message, error) { +func (l Light) Toggle(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return l.service.CallService( - ctx, "light", "toggle", serviceData, ga.EntityTarget(entityID), + ctx, "light", "toggle", serviceData, target, ) } -func (l Light) TurnOff(entityID string) (websocket.Message, error) { +func (l Light) TurnOff(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return l.service.CallService( - ctx, "light", "turn_off", nil, ga.EntityTarget(entityID), + ctx, "light", "turn_off", nil, target, ) } diff --git a/internal/services/lock.go b/internal/services/lock.go index b1031e3..17871d9 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -22,19 +22,19 @@ func NewLock(service Service) *Lock { /* Public API */ // Lock a lock entity. -func (l Lock) Lock(entityID string, serviceData any) (websocket.Message, error) { +func (l Lock) Lock(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return l.service.CallService( ctx, "lock", "lock", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Unlock a lock entity. -func (l Lock) Unlock(entityID string, serviceData any) (websocket.Message, error) { +func (l Lock) Unlock(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return l.service.CallService( ctx, "lock", "unlock", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 7efd1c2..660dc48 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -22,204 +22,204 @@ func NewMediaPlayer(service Service) *MediaPlayer { /* Public API */ // Send the media player the command to clear players playlist. -func (mp MediaPlayer) ClearPlaylist(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) ClearPlaylist(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "clear_playlist", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Group players together. Only works on platforms with support for player groups. -func (mp MediaPlayer) Join(entityID string, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) Join(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "join", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Send the media player the command for next track. -func (mp MediaPlayer) Next(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) Next(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_next_track", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Send the media player the command for pause. -func (mp MediaPlayer) Pause(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) Pause(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_pause", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Send the media player the command for play. -func (mp MediaPlayer) Play(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) Play(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_play", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Toggle media player play/pause state. -func (mp MediaPlayer) PlayPause(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) PlayPause(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_play_pause", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Send the media player the command for previous track. -func (mp MediaPlayer) Previous(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) Previous(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_previous_track", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Send the media player the command to seek in current playing media. -func (mp MediaPlayer) Seek(entityID string, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) Seek(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_seek", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Send the media player the stop command. -func (mp MediaPlayer) Stop(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) Stop(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "media_stop", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Send the media player the command for playing media. -func (mp MediaPlayer) PlayMedia(entityID string, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) PlayMedia(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "play_media", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Set repeat mode. -func (mp MediaPlayer) RepeatSet(entityID string, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) RepeatSet(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "repeat_set", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Send the media player the command to change sound mode. func (mp MediaPlayer) SelectSoundMode( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "select_sound_mode", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Send the media player the command to change input source. func (mp MediaPlayer) SelectSource( - entityID string, serviceData any, + target ga.Target, serviceData any, ) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "select_source", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Set shuffling state. -func (mp MediaPlayer) Shuffle(entityID string, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) Shuffle(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "shuffle_set", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Toggles a media player power state. -func (mp MediaPlayer) Toggle(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) Toggle(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "toggle", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Turn a media player power off. -func (mp MediaPlayer) TurnOff(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) TurnOff(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "turn_off", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Turn a media player power on. -func (mp MediaPlayer) TurnOn(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) TurnOn(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "turn_on", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Unjoin the player from a group. Only works on // platforms with support for player groups. -func (mp MediaPlayer) Unjoin(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) Unjoin(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "unjoin", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Turn a media player volume down. -func (mp MediaPlayer) VolumeDown(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) VolumeDown(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "volume_down", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Mute a media player's volume. -func (mp MediaPlayer) VolumeMute(entityID string, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) VolumeMute(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "volume_mute", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Set a media player's volume level. -func (mp MediaPlayer) VolumeSet(entityID string, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) VolumeSet(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "volume_set", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Turn a media player volume up. -func (mp MediaPlayer) VolumeUp(entityID string) (websocket.Message, error) { +func (mp MediaPlayer) VolumeUp(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return mp.service.CallService( ctx, "media_player", "volume_up", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/number.go b/internal/services/number.go index e007b43..03fd5e4 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -21,11 +21,11 @@ func NewNumber(service Service) *Number { /* Public API */ -func (ib Number) SetValue(entityID string, value float32) (websocket.Message, error) { +func (ib Number) SetValue(target ga.Target, value float32) (websocket.Message, error) { ctx := context.TODO() return ib.service.CallService( ctx, "number", "set_value", map[string]any{"value": value}, - ga.EntityTarget(entityID), + target, ) } diff --git a/internal/services/scene.go b/internal/services/scene.go index ae60457..7af643c 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -31,11 +31,11 @@ func (s Scene) Apply(serviceData any) (websocket.Message, error) { } // Create a scene entity. -func (s Scene) Create(entityID string, serviceData any) (websocket.Message, error) { +func (s Scene) Create(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "scene", "create", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } @@ -48,10 +48,10 @@ func (s Scene) Reload() (websocket.Message, error) { } // TurnOn a scene entity. -func (s Scene) TurnOn(entityID string, serviceData any) (websocket.Message, error) { +func (s Scene) TurnOn(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "scene", "turn_on", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } diff --git a/internal/services/script.go b/internal/services/script.go index 78d8071..6217cc2 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -22,20 +22,20 @@ func NewScript(service Service) *Script { /* Public API */ // Reload a script that was created in the HA UI. -func (s Script) Reload(entityID string) (websocket.Message, error) { +func (s Script) Reload(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "script", "reload", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Toggle a script that was created in the HA UI. -func (s Script) Toggle(entityID string) (websocket.Message, error) { +func (s Script) Toggle(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "script", "toggle", - nil, ga.EntityTarget(entityID), + nil, target, ) } @@ -49,10 +49,10 @@ func (s Script) TurnOff() (websocket.Message, error) { } // Turn on a script that was created in the HA UI. -func (s Script) TurnOn(entityID string) (websocket.Message, error) { +func (s Script) TurnOn(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "script", "turn_on", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/switch.go b/internal/services/switch.go index 3ff9566..2d23ff6 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -21,26 +21,26 @@ func NewSwitch(service Service) *Switch { /* Public API */ -func (s Switch) TurnOn(entityID string) (websocket.Message, error) { +func (s Switch) TurnOn(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "switch", "turn_on", - nil, ga.EntityTarget(entityID), + nil, target, ) } -func (s Switch) Toggle(entityID string) (websocket.Message, error) { +func (s Switch) Toggle(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "switch", "toggle", - nil, ga.EntityTarget(entityID), + nil, target, ) } -func (s Switch) TurnOff(entityID string) (websocket.Message, error) { +func (s Switch) TurnOff(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return s.service.CallService( ctx, "switch", "turn_off", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/tts.go b/internal/services/tts.go index a90ec19..71d1c1e 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -30,20 +30,20 @@ func (tts TTS) ClearCache() (websocket.Message, error) { } // Say something using text-to-speech on a media player with cloud. -func (tts TTS) CloudSay(entityID string, serviceData any) (websocket.Message, error) { +func (tts TTS) CloudSay(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return tts.service.CallService( ctx, "tts", "cloud_say", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Say something using text-to-speech on a media player with // google_translate. -func (tts TTS) GoogleTranslateSay(entityID string, serviceData any) (websocket.Message, error) { +func (tts TTS) GoogleTranslateSay(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return tts.service.CallService( ctx, "tts", "google_translate_say", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 168ad50..3b47229 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -22,100 +22,100 @@ func NewVacuum(service Service) *Vacuum { /* Public API */ // Tell the vacuum cleaner to do a spot clean-up. -func (v Vacuum) CleanSpot(entityID string) (websocket.Message, error) { +func (v Vacuum) CleanSpot(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "clean_spot", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Locate the vacuum cleaner robot. -func (v Vacuum) Locate(entityID string) (websocket.Message, error) { +func (v Vacuum) Locate(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "locate", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Pause the cleaning task. -func (v Vacuum) Pause(entityID string) (websocket.Message, error) { +func (v Vacuum) Pause(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "pause", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Tell the vacuum cleaner to return to its dock. -func (v Vacuum) ReturnToBase(entityID string) (websocket.Message, error) { +func (v Vacuum) ReturnToBase(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "return_to_base", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Send a raw command to the vacuum cleaner. -func (v Vacuum) SendCommand(entityID string, serviceData any) (websocket.Message, error) { +func (v Vacuum) SendCommand(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "send_command", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Set the fan speed of the vacuum cleaner. -func (v Vacuum) SetFanSpeed(entityID string, serviceData any) (websocket.Message, error) { +func (v Vacuum) SetFanSpeed(target ga.Target, serviceData any) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "set_fan_speed", - serviceData, ga.EntityTarget(entityID), + serviceData, target, ) } // Start or resume the cleaning task. -func (v Vacuum) Start(entityID string) (websocket.Message, error) { +func (v Vacuum) Start(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "start", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Start, pause, or resume the cleaning task. -func (v Vacuum) StartPause(entityID string) (websocket.Message, error) { +func (v Vacuum) StartPause(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "start_pause", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Stop the current cleaning task. -func (v Vacuum) Stop(entityID string) (websocket.Message, error) { +func (v Vacuum) Stop(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "stop", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Stop the current cleaning task and return to home. -func (v Vacuum) TurnOff(entityID string) (websocket.Message, error) { +func (v Vacuum) TurnOff(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "turn_off", - nil, ga.EntityTarget(entityID), + nil, target, ) } // Start a new cleaning task. -func (v Vacuum) TurnOn(entityID string) (websocket.Message, error) { +func (v Vacuum) TurnOn(target ga.Target) (websocket.Message, error) { ctx := context.TODO() return v.service.CallService( ctx, "vacuum", "turn_on", - nil, ga.EntityTarget(entityID), + nil, target, ) } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index cf1411e..c5c894c 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -23,7 +23,7 @@ func NewZWaveJS(service Service) *ZWaveJS { // ZWaveJS bulk_set_partial_config_parameters service. func (zw ZWaveJS) BulkSetPartialConfigParam( - entityID string, parameter int, value any, + target ga.Target, parameter int, value any, ) (websocket.Message, error) { ctx := context.TODO() return zw.service.CallService( @@ -32,6 +32,6 @@ func (zw ZWaveJS) BulkSetPartialConfigParam( "parameter": parameter, "value": value, }, - ga.EntityTarget(entityID), + target, ) } From aa8e2ed0e8342122878cf1f0d52b7b6b4a607c3d Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 16:24:01 +0200 Subject: [PATCH 065/103] State(): add methods `Latitude()` and `Longitude()` --- app/checkers_test.go | 8 ++++++++ app/state.go | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/app/checkers_test.go b/app/checkers_test.go index 9204528..59c18ce 100644 --- a/app/checkers_test.go +++ b/app/checkers_test.go @@ -15,6 +15,14 @@ type MockState struct { GetError bool } +func (s MockState) Latitude() float64 { + return 0.0 +} + +func (s MockState) Longitude() float64 { + return 0.0 +} + func (s MockState) AfterSunrise(_ ...DurationString) bool { return true } diff --git a/app/state.go b/app/state.go index e7e3370..76f6de4 100644 --- a/app/state.go +++ b/app/state.go @@ -11,6 +11,8 @@ import ( ) type State interface { + Latitude() float64 + Longitude() float64 AfterSunrise(...DurationString) bool BeforeSunrise(...DurationString) bool AfterSunset(...DurationString) bool @@ -67,6 +69,14 @@ func (s *StateImpl) getLatLong(c *http.HttpClient, homeZoneEntityID string) erro return nil } +func (s *StateImpl) Latitude() float64 { + return s.latitude +} + +func (s *StateImpl) Longitude() float64 { + return s.longitude +} + func (s *StateImpl) Get(entityID string) (EntityState, error) { resp, err := s.httpClient.GetState(entityID) if err != nil { From 8af6443668011ca37bc90284a0af2942cacbedda Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 16:25:14 +0200 Subject: [PATCH 066/103] App.state: change type from `*StateImpl` to `State` --- app/app.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/app.go b/app/app.go index cc3f010..f012180 100644 --- a/app/app.go +++ b/app/app.go @@ -30,7 +30,7 @@ type App struct { httpClient *http.HttpClient service *Service - state *StateImpl + state State scheduledActions priorityqueue.PriorityQueue entityListeners map[string][]*EntityListener @@ -258,11 +258,11 @@ func (app *App) RegisterEventListeners(evls ...EventListener) { } func getSunriseSunset( - s *StateImpl, sunrise bool, dateToUse carbon.Carbon, offset ...DurationString, + s State, sunrise bool, dateToUse carbon.Carbon, offset ...DurationString, ) carbon.Carbon { date := dateToUse.Carbon2Time() rise, set := sunriseLib.SunriseSunset( - s.latitude, s.longitude, date.Year(), date.Month(), date.Day(), + s.Latitude(), s.Longitude(), date.Year(), date.Month(), date.Day(), ) rise, set = rise.Local(), set.Local() From cdecbb762a9512a0ccdf1c3efd249cb6366e8de6 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 16:25:59 +0200 Subject: [PATCH 067/103] App: make fields `Service` and `State` public --- app/app.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/app.go b/app/app.go index f012180..edf5da8 100644 --- a/app/app.go +++ b/app/app.go @@ -29,8 +29,8 @@ type App struct { httpClient *http.HttpClient - service *Service - state State + Service *Service + State State scheduledActions priorityqueue.PriorityQueue entityListeners map[string][]*EntityListener @@ -115,13 +115,13 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { app := App{ wsConn: wsWriter, httpClient: httpClient, - state: state, + State: state, scheduledActions: priorityqueue.New(), entityListeners: map[string][]*EntityListener{}, eventListeners: map[string][]*EventListener{}, cancel: func() {}, } - app.service = newService(&app, httpClient) + app.Service = newService(&app, httpClient) return &app, nil } @@ -298,11 +298,11 @@ func getSunriseSunset( } func getNextSunRiseOrSet(app *App, sunrise bool, offset ...DurationString) carbon.Carbon { - sunriseOrSunset := getSunriseSunset(app.state, sunrise, carbon.Now(), offset...) + 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(app.state, sunrise, carbon.Tomorrow(), offset...) + sunriseOrSunset = getSunriseSunset(app.State, sunrise, carbon.Tomorrow(), offset...) } return sunriseOrSunset } @@ -345,7 +345,7 @@ func (app *App) Start(ctx context.Context) error { // ensure each ETL only runs once, even if // it listens to multiple entities if etl.runOnStartup && !etl.runOnStartupCompleted { - entityState, err := app.state.Get(eid) + entityState, err := app.State.Get(eid) if err != nil { slog.Warn( "Failed to get entity state \"", eid, @@ -355,7 +355,7 @@ func (app *App) Start(ctx context.Context) error { etl.runOnStartupCompleted = true eg.Go(func() error { - etl.callback(app.service, app.state, EntityData{ + etl.callback(app.Service, app.State, EntityData{ TriggerEntityID: eid, FromState: entityState.State, FromAttributes: entityState.Attributes, @@ -403,11 +403,11 @@ func (app *App) close() { } func (app *App) GetService() *Service { - return app.service + return app.Service } func (app *App) GetState() State { - return app.state + return app.State } func (app *App) runScheduledActions(ctx context.Context) { From 7fff90df4ae7f5e0d332e844b09afba27bf6f2ea Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 16:41:01 +0200 Subject: [PATCH 068/103] Change how callbacks are invoked Don't pass `app.Service` and `app.State` to the callbacks. They were added to the app at a point where the app was known, and those fields are public now, so they can access them by themselves rather than needing them to be passed in. --- app/app.go | 2 +- app/entitylistener.go | 10 +++++----- app/eventListener.go | 8 ++++---- app/interval.go | 8 ++++---- app/schedule.go | 8 ++++---- example/example.go | 35 +++++++++++++++++++++-------------- example/example_live_test.go | 4 ++-- 7 files changed, 41 insertions(+), 34 deletions(-) diff --git a/app/app.go b/app/app.go index edf5da8..0d4a855 100644 --- a/app/app.go +++ b/app/app.go @@ -355,7 +355,7 @@ func (app *App) Start(ctx context.Context) error { etl.runOnStartupCompleted = true eg.Go(func() error { - etl.callback(app.Service, app.State, EntityData{ + etl.callback(EntityData{ TriggerEntityID: eid, FromState: entityState.State, FromAttributes: entityState.Attributes, diff --git a/app/entitylistener.go b/app/entitylistener.go index acde43b..a2621e6 100644 --- a/app/entitylistener.go +++ b/app/entitylistener.go @@ -34,7 +34,7 @@ type EntityListener struct { disabledEntities []internal.EnabledDisabledInfo } -type EntityListenerCallback func(*Service, State, EntityData) +type EntityListenerCallback func(EntityData) type EntityData struct { TriggerEntityID string @@ -244,10 +244,10 @@ func (app *App) callEntityListeners(chanMsg websocket.Message) { if c := checkExceptionRanges(l.exceptionRanges); c.fail { continue } - if c := checkEnabledEntity(app.state, l.enabledEntities); c.fail { + if c := checkEnabledEntity(app.State, l.enabledEntities); c.fail { continue } - if c := checkDisabledEntity(app.state, l.disabledEntities); c.fail { + if c := checkDisabledEntity(app.State, l.disabledEntities); c.fail { continue } @@ -263,14 +263,14 @@ func (app *App) callEntityListeners(chanMsg websocket.Message) { if l.delay != 0 { l := l l.delayTimer = time.AfterFunc(l.delay, func() { - go l.callback(app.service, app.state, entityData) + go l.callback(entityData) l.lastRan = carbon.Now() }) continue } // run now if no delay set - go l.callback(app.service, app.state, entityData) + go l.callback(entityData) l.lastRan = carbon.Now() } } diff --git a/app/eventListener.go b/app/eventListener.go index febab01..dc9e143 100644 --- a/app/eventListener.go +++ b/app/eventListener.go @@ -26,7 +26,7 @@ type EventListener struct { disabledEntities []internal.EnabledDisabledInfo } -type EventListenerCallback func(*Service, State, EventData) +type EventListenerCallback func(EventData) type EventData struct { Type string @@ -182,10 +182,10 @@ func (app *App) callEventListeners(msg websocket.Message) { if c := checkExceptionRanges(l.exceptionRanges); c.fail { continue } - if c := checkEnabledEntity(app.state, l.enabledEntities); c.fail { + if c := checkEnabledEntity(app.State, l.enabledEntities); c.fail { continue } - if c := checkDisabledEntity(app.state, l.disabledEntities); c.fail { + if c := checkDisabledEntity(app.State, l.disabledEntities); c.fail { continue } @@ -193,7 +193,7 @@ func (app *App) callEventListeners(msg websocket.Message) { Type: baseEventMsg.Event.EventType, RawEventJSON: msg.Raw, } - go l.callback(app.service, app.state, eventData) + go l.callback(eventData) l.lastRan = carbon.Now() } } diff --git a/app/interval.go b/app/interval.go index 55d3dc2..067bf54 100644 --- a/app/interval.go +++ b/app/interval.go @@ -8,7 +8,7 @@ import ( "saml.dev/gome-assistant/internal" ) -type IntervalCallback func(*Service, State) +type IntervalCallback func() type Interval struct { frequency time.Duration @@ -189,17 +189,17 @@ func (i Interval) shouldRun(app *App) bool { if c := checkExceptionRanges(i.exceptionRanges); c.fail { return false } - if c := checkEnabledEntity(app.state, i.enabledEntities); c.fail { + if c := checkEnabledEntity(app.State, i.enabledEntities); c.fail { return false } - if c := checkDisabledEntity(app.state, i.disabledEntities); c.fail { + if c := checkDisabledEntity(app.State, i.disabledEntities); c.fail { return false } return true } func (i *Interval) run(app *App) { - i.callback(app.service, app.state) + i.callback() } func (i *Interval) updateNextRunTime(app *App) { diff --git a/app/schedule.go b/app/schedule.go index 085b582..2c58537 100644 --- a/app/schedule.go +++ b/app/schedule.go @@ -8,7 +8,7 @@ import ( "saml.dev/gome-assistant/internal" ) -type ScheduleCallback func(*Service, State) +type ScheduleCallback func() type DailySchedule struct { // 0-23 @@ -191,17 +191,17 @@ func (s *DailySchedule) shouldRun(app *App) bool { if c := checkAllowlistDates(s.allowlistDates); c.fail { return false } - if c := checkEnabledEntity(app.state, s.enabledEntities); c.fail { + if c := checkEnabledEntity(app.State, s.enabledEntities); c.fail { return false } - if c := checkDisabledEntity(app.state, s.disabledEntities); c.fail { + if c := checkDisabledEntity(app.State, s.disabledEntities); c.fail { return false } return true } func (s *DailySchedule) run(app *App) { - s.callback(app.service, app.state) + s.callback() } func (s *DailySchedule) updateNextRunTime(app *App) { diff --git a/example/example.go b/example/example.go index 5143390..8a4a8a0 100644 --- a/example/example.go +++ b/example/example.go @@ -8,6 +8,7 @@ import ( "time" ga "saml.dev/gome-assistant" + "saml.dev/gome-assistant/app" gaapp "saml.dev/gome-assistant/app" ) @@ -31,18 +32,24 @@ func main() { pantryDoor := gaapp. NewEntityListener(). EntityIDs("binary_sensor.pantry_door"). - Call(pantryLights). + Call(func(sensor gaapp.EntityData) { + pantryLights(app, sensor) + }). Build() _11pmSched := gaapp. NewDailySchedule(). - Call(lightsOut). + Call(func() { + lightsOut(app) + }). At("23:00"). Build() _30minsBeforeSunrise := gaapp. NewDailySchedule(). - Call(sunriseSched). + Call(func() { + sunriseSched(app) + }). Sunrise("-30m"). Build() @@ -59,16 +66,16 @@ func main() { app.Start(ctx) } -func pantryLights(service *gaapp.Service, state gaapp.State, sensor gaapp.EntityData) { +func pantryLights(app *app.App, sensor gaapp.EntityData) { l := ga.EntityTarget("light.pantry") if sensor.ToState == "on" { - service.HomeAssistant.TurnOn(l, nil) + app.Service.HomeAssistant.TurnOn(l, nil) } else { - service.HomeAssistant.TurnOff(l) + app.Service.HomeAssistant.TurnOff(l) } } -func onEvent(service *gaapp.Service, state gaapp.State, data gaapp.EventData) { +func onEvent(data gaapp.EventData) { // Since the structure of the event changes depending // on the event type, you can Unmarshal the raw json // into a Go type. If a type for your event doesn't @@ -79,10 +86,10 @@ func onEvent(service *gaapp.Service, state gaapp.State, data gaapp.EventData) { slog.Info("On event invoked", "event", ev) } -func lightsOut(service *gaapp.Service, state gaapp.State) { +func lightsOut(app *app.App) { // always turn off outside lights - service.Light.TurnOff(ga.EntityTarget("light.outside_lights")) - s, err := state.Get("binary_sensor.living_room_motion") + app.Service.Light.TurnOff(ga.EntityTarget("light.outside_lights")) + s, err := app.State.Get("binary_sensor.living_room_motion") if err != nil { slog.Warn("couldnt get living room motion state, doing nothing") return @@ -90,11 +97,11 @@ func lightsOut(service *gaapp.Service, state gaapp.State) { // if no motion detected in living room for 30mins if s.State == "off" && time.Since(s.LastChanged).Minutes() > 30 { - service.Light.TurnOff(ga.EntityTarget("light.main_lights")) + app.Service.Light.TurnOff(ga.EntityTarget("light.main_lights")) } } -func sunriseSched(service *gaapp.Service, state gaapp.State) { - service.Light.TurnOn(ga.EntityTarget("light.living_room_lamps"), nil) - service.Light.TurnOff(ga.EntityTarget("light.christmas_lights")) +func sunriseSched(app *app.App) { + app.Service.Light.TurnOn(ga.EntityTarget("light.living_room_lamps"), nil) + app.Service.Light.TurnOff(ga.EntityTarget("light.christmas_lights")) } diff --git a/example/example_live_test.go b/example/example_live_test.go index 8eaff4a..ce285d4 100644 --- a/example/example_live_test.go +++ b/example/example_live_test.go @@ -133,7 +133,7 @@ func (s *MySuite) TestSchedule() { } // Capture event after light entity state has changed -func (s *MySuite) entityCallback(se *gaapp.Service, st gaapp.State, e gaapp.EntityData) { +func (s *MySuite) entityCallback(e gaapp.EntityData) { slog.Info( "Entity callback called.", "entity id", e.TriggerEntityID, @@ -144,7 +144,7 @@ func (s *MySuite) entityCallback(se *gaapp.Service, st gaapp.State, e gaapp.Enti } // Capture planned daily schedule -func (s *MySuite) dailyScheduleCallback(se *gaapp.Service, st gaapp.State) { +func (s *MySuite) dailyScheduleCallback() { slog.Info("Daily schedule callback called.") s.suiteCtx["dailyScheduleCallbackInvoked"] = true } From f3e163d92d810aca17cef078ccc57cb77665bb00 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Tue, 30 Apr 2024 17:30:35 +0200 Subject: [PATCH 069/103] App: extract methods to register single listeners --- app/app.go | 64 +++++++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/app/app.go b/app/app.go index 0d4a855..fee2ff0 100644 --- a/app/app.go +++ b/app/app.go @@ -215,45 +215,51 @@ func (app *App) RegisterIntervals(intervals ...*Interval) { } } +func (app *App) RegisterEntityListener(etl EntityListener) { + if etl.delay != 0 && etl.toState == "" { + slog.Error("EntityListener error: you have to use ToState() when using Duration()") + panic(ErrInvalidArgs) + } + + for _, entity := range etl.entityIDs { + if elList, ok := app.entityListeners[entity]; ok { + app.entityListeners[entity] = append(elList, &etl) + } else { + app.entityListeners[entity] = []*EntityListener{&etl} + } + } +} + func (app *App) RegisterEntityListeners(etls ...EntityListener) { for _, etl := range etls { - etl := etl - if etl.delay != 0 && etl.toState == "" { - slog.Error("EntityListener error: you have to use ToState() when using Duration()") - panic(ErrInvalidArgs) - } + app.RegisterEntityListener(etl) + } +} - for _, entity := range etl.entityIDs { - if elList, ok := app.entityListeners[entity]; ok { - app.entityListeners[entity] = append(elList, &etl) - } else { - app.entityListeners[entity] = []*EntityListener{&etl} +func (app *App) RegisterEventListener(evl EventListener) { + for _, eventType := range evl.eventTypes { + elList, ok := app.eventListeners[eventType] + if !ok { + // FIXME: keep track of subscriptions so that they can + // be unsubscribed from. + _, err := app.WatchEvents( + eventType, + func(msg websocket.Message) { + go app.callEventListeners(msg) + }, + ) + if err != nil { + // FIXME: better error handling + panic(err) } } + app.eventListeners[eventType] = append(elList, &evl) } } func (app *App) RegisterEventListeners(evls ...EventListener) { for _, evl := range evls { - evl := evl - for _, eventType := range evl.eventTypes { - elList, ok := app.eventListeners[eventType] - if !ok { - // FIXME: keep track of subscriptions so that they can - // be unsubscribed from. - _, err := app.WatchEvents( - eventType, - func(msg websocket.Message) { - go app.callEventListeners(msg) - }, - ) - if err != nil { - // FIXME: better error handling - panic(err) - } - } - app.eventListeners[eventType] = append(elList, &evl) - } + app.RegisterEventListener(evl) } } From e3c0e86045f8e913ee54255738ee090fc82e6d64 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 16:59:06 +0200 Subject: [PATCH 070/103] Include the raw data in `EntityState` --- app/state.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/state.go b/app/state.go index 76f6de4..687a829 100644 --- a/app/state.go +++ b/app/state.go @@ -33,6 +33,9 @@ type EntityState struct { State string `json:"state"` Attributes map[string]any `json:"attributes"` LastChanged time.Time `json:"last_changed"` + + // The whole message, in JSON format: + Raw json.RawMessage `json:"-"` } func newState(c *http.HttpClient, homeZoneEntityID string) (*StateImpl, error) { @@ -84,6 +87,7 @@ func (s *StateImpl) Get(entityID string) (EntityState, error) { } es := EntityState{} json.Unmarshal(resp, &es) + es.Raw = resp return es, nil } From 33091fe4d8b7af0471a7517e55f24d6ba7ce7124 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 17:58:50 +0200 Subject: [PATCH 071/103] Rename two `App` methods: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * `App.WatchEvents()` → `SubscribeEvents()` * `App.WatchStateChangedEvents()` → `SubscribeStateChangedEvents()` --- app/app.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/app.go b/app/app.go index fee2ff0..865bbd7 100644 --- a/app/app.go +++ b/app/app.go @@ -242,7 +242,7 @@ func (app *App) RegisterEventListener(evl EventListener) { if !ok { // FIXME: keep track of subscriptions so that they can // be unsubscribed from. - _, err := app.WatchEvents( + _, err := app.SubscribeEvents( eventType, func(msg websocket.Message) { go app.callEventListeners(msg) @@ -332,7 +332,7 @@ func (app *App) Start(ctx context.Context) error { }) // subscribe to state_changed events - stateChangedSubscription, err := app.WatchStateChangedEvents( + stateChangedSubscription, err := app.SubscribeStateChangedEvents( func(msg websocket.Message) { go app.callEntityListeners(msg) }, @@ -462,11 +462,11 @@ type SubEvent struct { EventType string `json:"event_type"` } -// WatchEvents subscribes to events of the given type, invoking +// SubscribeEvents subscribes to events of the given type, invoking // `subscriber` when any such events are received. Calls to // `subscriber` are synchronous with respect to any other received // messages, but asynchronous with respect to writes. -func (app *App) WatchEvents( +func (app *App) SubscribeEvents( eventType string, subscriber websocket.Subscriber, ) (websocket.Subscription, error) { // Make sure we're listening before events might start arriving: @@ -494,10 +494,10 @@ func (app *App) WatchEvents( return subscription, nil } -func (app *App) WatchStateChangedEvents( +func (app *App) SubscribeStateChangedEvents( subscriber websocket.Subscriber, ) (websocket.Subscription, error) { - return app.WatchEvents("state_changed", subscriber) + return app.SubscribeEvents("state_changed", subscriber) } type UnsubEvent struct { From 7360b95cd74ab69a9be0324044788ba1f11ff587 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 18:03:16 +0200 Subject: [PATCH 072/103] app.go: reorder method definitions --- app/app.go | 86 +++++++++++++++++++++++++++--------------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/app/app.go b/app/app.go index 865bbd7..61a61d2 100644 --- a/app/app.go +++ b/app/app.go @@ -457,49 +457,6 @@ func (app *App) requeueScheduledAction(action scheduledAction) { app.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) } -type SubEvent struct { - websocket.BaseMessage - EventType string `json:"event_type"` -} - -// SubscribeEvents subscribes to events of the given type, invoking -// `subscriber` when any such events are received. Calls to -// `subscriber` are synchronous with respect to any other received -// messages, but asynchronous with respect to writes. -func (app *App) SubscribeEvents( - eventType string, subscriber websocket.Subscriber, -) (websocket.Subscription, error) { - // Make sure we're listening before events might start arriving: - e := SubEvent{ - BaseMessage: websocket.BaseMessage{ - Type: "subscribe_events", - }, - EventType: eventType, - } - var subscription websocket.Subscription - err := app.wsConn.Send(func(lc websocket.LockedConn) error { - subscription = lc.Subscribe(subscriber) - e.ID = subscription.ID() - if err := lc.SendMessage(e); err != nil { - lc.Unsubscribe(subscription) - return fmt.Errorf("error writing to websocket: %w", err) - } - return nil - }) - if err != nil { - return websocket.Subscription{}, err - } - // m, _ := ReadMessage(conn, ctx) - // log.Default().Println(string(m)) - return subscription, nil -} - -func (app *App) SubscribeStateChangedEvents( - subscriber websocket.Subscriber, -) (websocket.Subscription, error) { - return app.SubscribeEvents("state_changed", subscriber) -} - type UnsubEvent struct { websocket.BaseMessage Subscription int64 `json:"subscription"` @@ -673,3 +630,46 @@ func (app *App) Subscribe( return websocket.Message{}, websocket.Subscription{}, ctx.Err() } } + +type SubEvent struct { + websocket.BaseMessage + EventType string `json:"event_type"` +} + +// SubscribeEvents subscribes to events of the given type, invoking +// `subscriber` when any such events are received. Calls to +// `subscriber` are synchronous with respect to any other received +// messages, but asynchronous with respect to writes. +func (app *App) SubscribeEvents( + eventType string, subscriber websocket.Subscriber, +) (websocket.Subscription, error) { + // Make sure we're listening before events might start arriving: + e := SubEvent{ + BaseMessage: websocket.BaseMessage{ + Type: "subscribe_events", + }, + EventType: eventType, + } + var subscription websocket.Subscription + err := app.wsConn.Send(func(lc websocket.LockedConn) error { + subscription = lc.Subscribe(subscriber) + e.ID = subscription.ID() + if err := lc.SendMessage(e); err != nil { + lc.Unsubscribe(subscription) + return fmt.Errorf("error writing to websocket: %w", err) + } + return nil + }) + if err != nil { + return websocket.Subscription{}, err + } + // m, _ := ReadMessage(conn, ctx) + // log.Default().Println(string(m)) + return subscription, nil +} + +func (app *App) SubscribeStateChangedEvents( + subscriber websocket.Subscriber, +) (websocket.Subscription, error) { + return app.SubscribeEvents("state_changed", subscriber) +} From 7b42441bc8a7c72018ddf25dc2d35825d71c5fc8 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 18:04:42 +0200 Subject: [PATCH 073/103] subscribeEventsRequest: rename from `SubEvent` --- app/app.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/app.go b/app/app.go index 61a61d2..704d4a5 100644 --- a/app/app.go +++ b/app/app.go @@ -631,7 +631,7 @@ func (app *App) Subscribe( } } -type SubEvent struct { +type subscribeEventsRequest struct { websocket.BaseMessage EventType string `json:"event_type"` } @@ -644,7 +644,7 @@ func (app *App) SubscribeEvents( eventType string, subscriber websocket.Subscriber, ) (websocket.Subscription, error) { // Make sure we're listening before events might start arriving: - e := SubEvent{ + e := subscribeEventsRequest{ BaseMessage: websocket.BaseMessage{ Type: "subscribe_events", }, From 8d29eb2f7d63ae0c503d946ead432916bf924430 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 12 May 2024 11:58:53 +0200 Subject: [PATCH 074/103] Target.String(): new method --- target.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/target.go b/target.go index 2be15be..243a49c 100644 --- a/target.go +++ b/target.go @@ -1,5 +1,7 @@ package ga +import "fmt" + // Target represents the target of the service call, if applicable. type Target struct { EntityID string `json:"entity_id,omitempty"` @@ -17,3 +19,14 @@ func DeviceTarget(deviceID string) Target { DeviceID: deviceID, } } + +func (t Target) String() string { + switch { + case t.EntityID != "": + return fmt.Sprintf("entity %s", t.EntityID) + case t.DeviceID != "": + return fmt.Sprintf("device %s", t.DeviceID) + default: + return "unset target" + } +} From 9f96127b7e007a1bcb202d65b1207cdf99fb86b1 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 18:41:33 +0200 Subject: [PATCH 075/103] Improve how raw messages are output --- app/app.go | 7 ++++--- app/state.go | 3 ++- websocket/message.go | 16 +++++++++++++++- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/app/app.go b/app/app.go index 704d4a5..f65727f 100644 --- a/app/app.go +++ b/app/app.go @@ -637,9 +637,10 @@ type subscribeEventsRequest struct { } // SubscribeEvents subscribes to events of the given type, invoking -// `subscriber` when any such events are received. Calls to -// `subscriber` are synchronous with respect to any other received -// messages, but asynchronous with respect to writes. +// `subscriber` when any such events are received. `eventType` can be +// `*` to listen to all event types. Calls to `subscriber` are +// synchronous with respect to any other received messages, but +// asynchronous with respect to writes. func (app *App) SubscribeEvents( eventType string, subscriber websocket.Subscriber, ) (websocket.Subscription, error) { diff --git a/app/state.go b/app/state.go index 687a829..977e45a 100644 --- a/app/state.go +++ b/app/state.go @@ -8,6 +8,7 @@ import ( "github.com/golang-module/carbon" "saml.dev/gome-assistant/internal/http" + "saml.dev/gome-assistant/websocket" ) type State interface { @@ -35,7 +36,7 @@ type EntityState struct { LastChanged time.Time `json:"last_changed"` // The whole message, in JSON format: - Raw json.RawMessage `json:"-"` + Raw websocket.RawMessage `json:"-"` } func newState(c *http.HttpClient, homeZoneEntityID string) (*StateImpl, error) { diff --git a/websocket/message.go b/websocket/message.go index e43deae..74fc578 100644 --- a/websocket/message.go +++ b/websocket/message.go @@ -4,6 +4,20 @@ import ( "encoding/json" ) +// RawMessage is like `json.RawMessage`, except that its `String()` +// method converts it directly to a string. +type RawMessage json.RawMessage + +// UnmarshalJSON delegates to `json.RawMessage`. (The method has a +// pointer receiver, so we have to implement it explicitly.) +func (m *RawMessage) UnmarshalJSON(data []byte) error { + return (*json.RawMessage)(m).UnmarshalJSON(data) +} + +func (rm RawMessage) String() string { + return string(rm) +} + // BaseMessage implements the required part of any websocket message. // The idea is to embed this type in other message types. type BaseMessage struct { @@ -36,5 +50,5 @@ type Message struct { // Raw contains the original, full, unparsed message (including // fields `Type` and `ID`, which appear in `BaseMessage`). - Raw json.RawMessage + Raw RawMessage } From fe7913fefbaaa503127bcba54c8619e04131e85d Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 5 May 2024 00:04:00 +0200 Subject: [PATCH 076/103] Message: avoid inadvertently filling the `Raw` field from the JSON The `Raw` field holds the entire message, so make sure that it is not read from a JSON field that happens to have that name. --- websocket/message.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/websocket/message.go b/websocket/message.go index 74fc578..db357ba 100644 --- a/websocket/message.go +++ b/websocket/message.go @@ -49,6 +49,6 @@ type Message struct { BaseMessage // Raw contains the original, full, unparsed message (including - // fields `Type` and `ID`, which appear in `BaseMessage`). - Raw RawMessage + // fields `Type` and `ID`, which also appear in `BaseMessage`). + Raw RawMessage `json:"-"` } From 890f13ebef4f9aa2320bdf292050e6d678a8880c Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 20:04:27 +0200 Subject: [PATCH 077/103] Subscribe(): no need to wrap the incoming context --- app/app.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/app.go b/app/app.go index f65727f..7e9aaa7 100644 --- a/app/app.go +++ b/app/app.go @@ -582,9 +582,6 @@ func (app *App) CallService( func (app *App) Subscribe( ctx context.Context, req websocket.Request, subscriber websocket.Subscriber, ) (websocket.Message, websocket.Subscription, error) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - // The result of the attempt to subscribe (i.e., the first // message) will be sent to this channel. resultReceived := false From 7a3f7308c6cc1c773785a2e467ccbfd440de563d Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 21:00:35 +0200 Subject: [PATCH 078/103] Add a way to tell when the app is ready for use --- app/app.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/app.go b/app/app.go index 7e9aaa7..9af95b0 100644 --- a/app/app.go +++ b/app/app.go @@ -36,6 +36,9 @@ type App struct { entityListeners map[string][]*EntityListener eventListeners map[string][]*EventListener + // Ready is closed when the app is ready for use. + ready chan struct{} + // If `App.Start()` has been called, `cancel()` cancels the // context being used, which causes the app to shut down cleanly. cancel context.CancelFunc @@ -119,6 +122,7 @@ func NewAppFromConfig(ctx context.Context, config NewAppConfig) (*App, error) { scheduledActions: priorityqueue.New(), entityListeners: map[string][]*EntityListener{}, eventListeners: map[string][]*EventListener{}, + ready: make(chan struct{}), cancel: func() {}, } app.Service = newService(&app, httpClient) @@ -188,6 +192,11 @@ func NewApp(ctx context.Context, request NewAppRequest) (*App, error) { return NewAppFromConfig(ctx, config) } +// Ready returns a channel that is closed when the app is ready for use. +func (app *App) Ready() <-chan struct{} { + return app.ready +} + type scheduledAction interface { String() string Hash() string @@ -382,6 +391,8 @@ func (app *App) Start(ctx context.Context) error { return nil }) + close(app.ready) + eg.Go(func() error { <-ctx.Done() app.Close() From 0789d28454384c93327f474f31d4ef7e4ad73286 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 20:13:29 +0200 Subject: [PATCH 079/103] Start the `websocket.Conn` earlier in the initialization sequence --- app/app.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/app.go b/app/app.go index 9af95b0..d2fc096 100644 --- a/app/app.go +++ b/app/app.go @@ -335,6 +335,13 @@ func (app *App) Start(ctx context.Context) error { slog.Info("Starting", "entity listeners", len(app.entityListeners)) slog.Info("Starting", "event listeners", len(app.eventListeners)) + // entity listeners and event listeners + eg.Go(func() error { + app.wsConn.Start() + cancel() + return nil + }) + eg.Go(func() error { app.runScheduledActions(ctx) return nil @@ -384,13 +391,6 @@ func (app *App) Start(ctx context.Context) error { } } - // entity listeners and event listeners - eg.Go(func() error { - app.wsConn.Start() - cancel() - return nil - }) - close(app.ready) eg.Go(func() error { From 753f589b043042c380ada225b22174a34e968903 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 11 May 2024 20:05:06 +0200 Subject: [PATCH 080/103] App.SubscribeEvents(): use `App.Subscribe()` in implementation --- app/app.go | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/app/app.go b/app/app.go index d2fc096..6abf56c 100644 --- a/app/app.go +++ b/app/app.go @@ -652,6 +652,9 @@ type subscribeEventsRequest struct { func (app *App) SubscribeEvents( eventType string, subscriber websocket.Subscriber, ) (websocket.Subscription, error) { + ctx, cancel := context.WithTimeout(context.TODO(), 10*time.Second) + defer cancel() + // Make sure we're listening before events might start arriving: e := subscribeEventsRequest{ BaseMessage: websocket.BaseMessage{ @@ -659,21 +662,15 @@ func (app *App) SubscribeEvents( }, EventType: eventType, } - var subscription websocket.Subscription - err := app.wsConn.Send(func(lc websocket.LockedConn) error { - subscription = lc.Subscribe(subscriber) - e.ID = subscription.ID() - if err := lc.SendMessage(e); err != nil { - lc.Unsubscribe(subscription) - return fmt.Errorf("error writing to websocket: %w", err) - } - return nil - }) + + response, subscription, err := app.Subscribe(ctx, &e, subscriber) if err != nil { return websocket.Subscription{}, err } - // m, _ := ReadMessage(conn, ctx) - // log.Default().Println(string(m)) + + // FIXME: check response for success + _ = response + return subscription, nil } From 4fc05c139d89e35dff864437a1533942446b8a28 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 12 May 2024 13:34:30 +0200 Subject: [PATCH 081/103] Conn.Start(): rename local variable Avoid confusion with the `bytes` package in the stdlib. --- websocket/read.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/websocket/read.go b/websocket/read.go index e260db1..be1ead0 100644 --- a/websocket/read.go +++ b/websocket/read.go @@ -11,7 +11,7 @@ import ( // there is an error reading from `conn`, log it and return. func (conn *Conn) Start() { for { - bytes, err := conn.readMessage() + b, err := conn.readMessage() if err != nil { slog.Error("Error reading from websocket:", err) return @@ -21,13 +21,13 @@ func (conn *Conn) Start() { // default to true for messages that don't include "success" at all Success: true, } - json.Unmarshal(bytes, &base) + json.Unmarshal(b, &base) if !base.Success { - slog.Warn("Received unsuccessful response", "response", string(bytes)) + slog.Warn("Received unsuccessful response", "response", string(b)) } msg := Message{ BaseMessage: base.BaseMessage, - Raw: bytes, + Raw: b, } if subscriber, ok := conn.getSubscriber(msg.ID); ok { From 965a857248ba73959aa0460341380d7c38e5acc3 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 12 May 2024 13:35:26 +0200 Subject: [PATCH 082/103] Conn.Start(): not all messages will be result messages Let the subscribers worry about the contents of the messages. --- websocket/read.go | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/websocket/read.go b/websocket/read.go index be1ead0..bef256d 100644 --- a/websocket/read.go +++ b/websocket/read.go @@ -17,18 +17,14 @@ func (conn *Conn) Start() { return } - base := BaseResultMessage{ - // default to true for messages that don't include "success" at all - Success: true, - } - json.Unmarshal(b, &base) - if !base.Success { - slog.Warn("Received unsuccessful response", "response", string(b)) - } - msg := Message{ - BaseMessage: base.BaseMessage, - Raw: b, + var msg Message + if err := json.Unmarshal(b, &msg); err != nil { + slog.Error("Error parsing JSON message from websocket:", err) + return } + // We've only deserialized part of the message, so store the + // raw bytes as well, so that the listeners can handle them. + msg.Raw = b if subscriber, ok := conn.getSubscriber(msg.ID); ok { subscriber(msg) From f94d31b88fcae4c6facac8bd2f9a6d7607ab2501 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 5 May 2024 00:11:47 +0200 Subject: [PATCH 083/103] websocket.ResultMessage: new type * Add a new type, `ResultError`, for capturing the information send by the server if there is an error satisfying a request. This type implements `error`. * Add a new type, `ResultMessage`, for storing lightly-parsed result messages plus the raw bytes of the `result` member, and a `*ResultError` for the event that the request failed. * Add a method `Message.GetResult()`, which parses the message as a `result` message, unmarshaling the result into a user-provided object, and returning an error if `success == false`. These new types are not yet used. --- websocket/message.go | 67 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/websocket/message.go b/websocket/message.go index db357ba..3029a42 100644 --- a/websocket/message.go +++ b/websocket/message.go @@ -2,6 +2,7 @@ package websocket import ( "encoding/json" + "fmt" ) // RawMessage is like `json.RawMessage`, except that its `String()` @@ -38,11 +39,6 @@ func (msg *BaseMessage) SetID(id int64) { msg.ID = id } -type BaseResultMessage struct { - BaseMessage - Success bool `json:"success"` -} - // Message holds a complete message, only partly parsed. The entire, // original, unparsed message is available in the `Raw` field. type Message struct { @@ -52,3 +48,64 @@ type Message struct { // fields `Type` and `ID`, which also appear in `BaseMessage`). Raw RawMessage `json:"-"` } + +type BaseResultMessage struct { + BaseMessage + Success bool `json:"success"` + Error *ResultError `json:"error,omitempty"` +} + +type ResultError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func (err *ResultError) Error() string { + switch { + case err.Code != "" && err.Message != "": + return fmt.Sprintf("%s: %s", err.Code, err.Message) + case err.Code == "" && err.Message != "": + return fmt.Sprintf("unknown_error: %s", err.Message) + case err.Code != "" && err.Message == "": + return fmt.Sprintf("%s", err.Code) + default: + // This seems not to be an error at all. + return fmt.Sprintf("INVALID (seems not to be an error)") + } +} + +type ResultMessage struct { + BaseResultMessage + + // Raw contains the "result" part of the message, unparsed. + Result RawMessage `json:"result"` +} + +// GetResult parses a result out of `msg` (which must have type +// "result"). If `msg` indicates that an error occurred, convert that +// to an error and return it. Parse the result into `result`, which +// must be unmarshalable as JSON. +func (msg Message) GetResult(result any) error { + if msg.Type != "result" { + return fmt.Errorf( + "response message was not of type 'result': %#v", msg, + ) + } + var resultMsg ResultMessage + if err := json.Unmarshal(msg.Raw, &resultMsg); err != nil { + return fmt.Errorf("unmarshaling result message: %w", err) + } + if !resultMsg.Success { + if resultMsg.Error == nil { + return fmt.Errorf( + "request did not succeed but no error was returned", + ) + } + return resultMsg.Error + } + + if err := json.Unmarshal(resultMsg.Result, result); err != nil { + return fmt.Errorf("unmarshalling result from %q: %w", resultMsg.Result, err) + } + return nil +} From 2815fc90d8edb6e3b3a9d16d917388965e45441c Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 12 May 2024 14:01:07 +0200 Subject: [PATCH 084/103] app/calls.go: new file, extracted from `app.go` --- app/app.go | 142 ------------------------------------------------ app/calls.go | 150 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 142 deletions(-) create mode 100644 app/calls.go diff --git a/app/app.go b/app/app.go index 6abf56c..1a87b83 100644 --- a/app/app.go +++ b/app/app.go @@ -12,7 +12,6 @@ import ( sunriseLib "github.com/nathan-osman/go-sunrise" "golang.org/x/sync/errgroup" - ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/internal/http" "saml.dev/gome-assistant/internal/priorityqueue" "saml.dev/gome-assistant/websocket" @@ -498,147 +497,6 @@ func (app *App) unwatchEvents(subscription websocket.Subscription) error { return nil } -// Call invokes an RPC service corresponding to `req` via websockets -// and waits for and returns a single `result`. `msg` must be -// serializable to JSON. It shouldn't have its ID filled in yet; that -// will be done within this method. The response is not analyzed at -// all, even to check for errors. -func (app *App) Call( - ctx context.Context, req websocket.Request, -) (websocket.Message, error) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - responseCh := make(chan websocket.Message, 1) - - var subscription websocket.Subscription - - // Receive a single message, sent it to `responseCh`, then - // unsubscribe: - subscriber := func(msg websocket.Message) { - defer close(responseCh) - responseCh <- msg - _ = app.wsConn.Send(func(lc websocket.LockedConn) error { - lc.Unsubscribe(subscription) - return nil - }) - } - - err := app.wsConn.Send(func(lc websocket.LockedConn) error { - subscription = lc.Subscribe(subscriber) - req.SetID(subscription.ID()) - if err := lc.SendMessage(req); err != nil { - lc.Unsubscribe(subscription) - return fmt.Errorf("error writing to websocket: %w", err) - } - return nil - }) - - if err != nil { - return websocket.Message{}, err - } - - select { - case response := <-responseCh: - return response, nil - case <-ctx.Done(): - return websocket.Message{}, ctx.Err() - } -} - -type CallServiceRequest struct { - websocket.BaseMessage - Domain string `json:"domain"` - Service string `json:"service"` - - // ServiceData must be serializable to a JSON object. - ServiceData any `json:"service_data,omitempty"` - - Target ga.Target `json:"target,omitempty"` -} - -// CallService invokes a service using a `call_service` message, then -// waits for and returns the response. -// -// FIXME: can the response be parsed into a result-style message? -func (app *App) CallService( - ctx context.Context, domain string, service string, serviceData any, target ga.Target, -) (websocket.Message, error) { - req := CallServiceRequest{ - BaseMessage: websocket.BaseMessage{ - Type: "call_service", - }, - Domain: domain, - Service: service, - ServiceData: serviceData, - Target: target, - } - - return app.Call(ctx, &req) -} - -// Subscribe subscribes to some events via `req`, waits for a single -// response, and then leaves `subscriber` subscribed to the events. If -// this method returns without an error, `subscriber` must eventually -// be unsubscribed. `ctx` covers the subscription and the wait for the -// first answer, but not the forwarding of subsequent events or -// unsubscribing. -// -// FIXME: should this subscriber and subscription be specialized to -// event messages? -// -// FIXME: should the result be examined? If the subscription request -// failed, then we could fail more generally instead of leaving the -// cleanup to the caller. -func (app *App) Subscribe( - ctx context.Context, req websocket.Request, subscriber websocket.Subscriber, -) (websocket.Message, websocket.Subscription, error) { - // The result of the attempt to subscribe (i.e., the first - // message) will be sent to this channel. - resultReceived := false - resultCh := make(chan websocket.Message, 1) - - var subscription websocket.Subscription - - // Receive a single message, sent it to `responseCh`, then - // unsubscribe: - dualSubscriber := func(msg websocket.Message) { - if !resultReceived { - // This is the first message. We send it to the channel so - // that it can be returned from the outer function. - defer close(resultCh) - resultCh <- msg - resultReceived = true - return - } - - // The result has already been processed. Subsequent events - // get forwarded to `subscriber`: - subscriber(msg) - } - - err := app.wsConn.Send(func(lc websocket.LockedConn) error { - subscription = lc.Subscribe(dualSubscriber) - req.SetID(subscription.ID()) - if err := lc.SendMessage(req); err != nil { - lc.Unsubscribe(subscription) - return fmt.Errorf("error writing to websocket: %w", err) - } - return nil - }) - - if err != nil { - return websocket.Message{}, websocket.Subscription{}, err - } - - select { - case response := <-resultCh: - return response, subscription, nil - case <-ctx.Done(): - return websocket.Message{}, websocket.Subscription{}, ctx.Err() - } -} - type subscribeEventsRequest struct { websocket.BaseMessage EventType string `json:"event_type"` diff --git a/app/calls.go b/app/calls.go new file mode 100644 index 0000000..dfa0261 --- /dev/null +++ b/app/calls.go @@ -0,0 +1,150 @@ +package app + +import ( + "context" + "fmt" + + ga "saml.dev/gome-assistant" + "saml.dev/gome-assistant/websocket" +) + +// Call invokes an RPC service corresponding to `req` via websockets +// and waits for and returns a single `result`. `msg` must be +// serializable to JSON. It shouldn't have its ID filled in yet; that +// will be done within this method. The response is not analyzed at +// all, even to check for errors. +func (app *App) Call( + ctx context.Context, req websocket.Request, +) (websocket.Message, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + responseCh := make(chan websocket.Message, 1) + + var subscription websocket.Subscription + + // Receive a single message, sent it to `responseCh`, then + // unsubscribe: + subscriber := func(msg websocket.Message) { + defer close(responseCh) + responseCh <- msg + _ = app.wsConn.Send(func(lc websocket.LockedConn) error { + lc.Unsubscribe(subscription) + return nil + }) + } + + err := app.wsConn.Send(func(lc websocket.LockedConn) error { + subscription = lc.Subscribe(subscriber) + req.SetID(subscription.ID()) + if err := lc.SendMessage(req); err != nil { + lc.Unsubscribe(subscription) + return fmt.Errorf("error writing to websocket: %w", err) + } + return nil + }) + + if err != nil { + return websocket.Message{}, err + } + + select { + case response := <-responseCh: + return response, nil + case <-ctx.Done(): + return websocket.Message{}, ctx.Err() + } +} + +type CallServiceRequest struct { + websocket.BaseMessage + Domain string `json:"domain"` + Service string `json:"service"` + + // ServiceData must be serializable to a JSON object. + ServiceData any `json:"service_data,omitempty"` + + Target ga.Target `json:"target,omitempty"` +} + +// CallService invokes a service using a `call_service` message, then +// waits for and returns the response. +// +// FIXME: can the response be parsed into a result-style message? +func (app *App) CallService( + ctx context.Context, domain string, service string, serviceData any, target ga.Target, +) (websocket.Message, error) { + req := CallServiceRequest{ + BaseMessage: websocket.BaseMessage{ + Type: "call_service", + }, + Domain: domain, + Service: service, + ServiceData: serviceData, + Target: target, + } + + return app.Call(ctx, &req) +} + +// Subscribe subscribes to some events via `req`, waits for a single +// response, and then leaves `subscriber` subscribed to the events. If +// this method returns without an error, `subscriber` must eventually +// be unsubscribed. `ctx` covers the subscription and the wait for the +// first answer, but not the forwarding of subsequent events or +// unsubscribing. +// +// FIXME: should this subscriber and subscription be specialized to +// event messages? +// +// FIXME: should the result be examined? If the subscription request +// failed, then we could fail more generally instead of leaving the +// cleanup to the caller. +func (app *App) Subscribe( + ctx context.Context, req websocket.Request, subscriber websocket.Subscriber, +) (websocket.Message, websocket.Subscription, error) { + // The result of the attempt to subscribe (i.e., the first + // message) will be sent to this channel. + resultReceived := false + resultCh := make(chan websocket.Message, 1) + + var subscription websocket.Subscription + + // Receive a single message, sent it to `responseCh`, then + // unsubscribe: + dualSubscriber := func(msg websocket.Message) { + if !resultReceived { + // This is the first message. We send it to the channel so + // that it can be returned from the outer function. + defer close(resultCh) + resultCh <- msg + resultReceived = true + return + } + + // The result has already been processed. Subsequent events + // get forwarded to `subscriber`: + subscriber(msg) + } + + err := app.wsConn.Send(func(lc websocket.LockedConn) error { + subscription = lc.Subscribe(dualSubscriber) + req.SetID(subscription.ID()) + if err := lc.SendMessage(req); err != nil { + lc.Unsubscribe(subscription) + return fmt.Errorf("error writing to websocket: %w", err) + } + return nil + }) + + if err != nil { + return websocket.Message{}, websocket.Subscription{}, err + } + + select { + case response := <-resultCh: + return response, subscription, nil + case <-ctx.Done(): + return websocket.Message{}, websocket.Subscription{}, ctx.Err() + } +} From c61c702bcdb91c4336bcf2d8ece9fe9544a1a8e8 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 12 May 2024 14:01:28 +0200 Subject: [PATCH 085/103] Change the service calling convention When calling a service, the caller isn't interested in the whole result message. All they really need to know is if there was an error, and if not, what was the result (without the message envelope). Among other things, the makes it easier to handle RPC errors more systematically. So, change the low-level `App.Call*()` methods to take an additional pointer to an object as argument. If the call is successful, the `result` member of the result message will be deserialized into this object using `json.Unmarshal()`. If, on the other hand, the result has `success == false`, then convert this into a `ResultError` and return that. Change the concrete service types to return only the `result` part of the result message. In most cases, this is declared as type `any`. If people go to the trouble of figuring out the structure of the methods' responses, these return types can be made more specific. --- app/app.go | 64 +++--- app/calls.go | 122 +++++------ app/result_subscriber.go | 82 ++++++++ internal/services/alarm_control_panel.go | 90 +++++---- internal/services/climate.go | 25 ++- internal/services/cover.go | 111 +++++++--- internal/services/event.go | 5 +- internal/services/homeassistant.go | 34 +++- internal/services/input_boolean.go | 45 +++-- internal/services/input_button.go | 23 ++- internal/services/input_datetime.go | 23 ++- internal/services/input_number.go | 45 +++-- internal/services/input_text.go | 23 ++- internal/services/light.go | 34 +++- internal/services/lock.go | 23 ++- internal/services/media_player.go | 247 ++++++++++++++++------- internal/services/notify.go | 12 +- internal/services/number.go | 12 +- internal/services/scene.go | 45 +++-- internal/services/script.go | 45 +++-- internal/services/services.go | 7 +- internal/services/switch.go | 34 +++- internal/services/tts.go | 34 +++- internal/services/vacuum.go | 122 +++++++---- internal/services/zwavejs.go | 12 +- 25 files changed, 914 insertions(+), 405 deletions(-) create mode 100644 app/result_subscriber.go diff --git a/app/app.go b/app/app.go index 1a87b83..5bbb26a 100644 --- a/app/app.go +++ b/app/app.go @@ -356,7 +356,7 @@ func (app *App) Start(ctx context.Context) error { return fmt.Errorf("subscribing to 'state_changed' events: %w", err) } - defer app.unwatchEvents(stateChangedSubscription) + defer app.UnsubscribeEvents(stateChangedSubscription) // entity listeners runOnStartup for eid, etls := range app.entityListeners { @@ -467,36 +467,6 @@ func (app *App) requeueScheduledAction(action scheduledAction) { app.scheduledActions.Insert(action, float64(action.getNextRunTime().Unix())) } -type UnsubEvent struct { - websocket.BaseMessage - Subscription int64 `json:"subscription"` -} - -// unwatchEvents unsubscribes to events with the given `subscriptionID`. This does -// not remove the subscriber. -func (app *App) unwatchEvents(subscription websocket.Subscription) error { - e := UnsubEvent{ - BaseMessage: websocket.BaseMessage{ - Type: "unsubscribe_events", - }, - Subscription: subscription.ID(), - } - - err := app.wsConn.Send(func(lc websocket.LockedConn) error { - lc.Unsubscribe(subscription) - - e.ID = lc.NextID() - return lc.SendMessage(e) - }) - if err != nil { - return fmt.Errorf("unsubscribing from ID %d: %w", subscription.ID(), err) - } - - // m, _ := ReadMessage(conn, ctx) - // log.Default().Println(string(m)) - return nil -} - type subscribeEventsRequest struct { websocket.BaseMessage EventType string `json:"event_type"` @@ -537,3 +507,35 @@ func (app *App) SubscribeStateChangedEvents( ) (websocket.Subscription, error) { return app.SubscribeEvents("state_changed", subscriber) } + +type unsubscribeEventsRequest struct { + websocket.BaseMessage + Subscription int64 `json:"subscription"` +} + +// UnsubscribeEvents unsubscribes, at the server, from events that +// were subscribed to via the specified `subscription`. +func (app *App) UnsubscribeEvents(subscription websocket.Subscription) error { + ctx := context.TODO() + + req := unsubscribeEventsRequest{ + BaseMessage: websocket.BaseMessage{ + Type: "unsubscribe_events", + }, + Subscription: subscription.ID(), + } + + var result any + rs := newResultSubscriber(app, &result) + err := app.wsConn.Send(func(lc websocket.LockedConn) error { + lc.Unsubscribe(subscription) + // Subscribe, so that we receive the result of the unsubscribe + // command itself: + return rs.subscribe(lc, &req) + }) + if err != nil { + return fmt.Errorf("unsubscribing from ID %d: %w", subscription.ID(), err) + } + + return rs.wait(ctx) +} diff --git a/app/calls.go b/app/calls.go index dfa0261..2508d80 100644 --- a/app/calls.go +++ b/app/calls.go @@ -2,58 +2,41 @@ package app import ( "context" + "encoding/json" "fmt" + "log/slog" ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/websocket" ) -// Call invokes an RPC service corresponding to `req` via websockets -// and waits for and returns a single `result`. `msg` must be -// serializable to JSON. It shouldn't have its ID filled in yet; that -// will be done within this method. The response is not analyzed at -// all, even to check for errors. +// Call invokes an RPC and processes the result as follows: +// 1. Generate a message ID. +// 2. Subscribe to that ID. +// 3. Send `req` over the websocket +// 4. Waits for a single "result" message +// 5. Unsubscribe from ID +// 6. Unmarshal the result into `result`. +// +// `msg` must be serializable to JSON. It shouldn't have its ID filled +// in yet; that will be done within this method. `result` must be +// something that `json.Unmarshal()` can deserialize into; typically, +// it is a pointer. If the result indicates a failure +// (success==false), then return that as an error. func (app *App) Call( - ctx context.Context, req websocket.Request, -) (websocket.Message, error) { + ctx context.Context, req websocket.Request, result any, +) error { ctx, cancel := context.WithCancel(ctx) defer cancel() - responseCh := make(chan websocket.Message, 1) - - var subscription websocket.Subscription - - // Receive a single message, sent it to `responseCh`, then - // unsubscribe: - subscriber := func(msg websocket.Message) { - defer close(responseCh) - responseCh <- msg - _ = app.wsConn.Send(func(lc websocket.LockedConn) error { - lc.Unsubscribe(subscription) - return nil - }) - } - + rs := newResultSubscriber(app, result) err := app.wsConn.Send(func(lc websocket.LockedConn) error { - subscription = lc.Subscribe(subscriber) - req.SetID(subscription.ID()) - if err := lc.SendMessage(req); err != nil { - lc.Unsubscribe(subscription) - return fmt.Errorf("error writing to websocket: %w", err) - } - return nil + return rs.subscribe(lc, req) }) - if err != nil { - return websocket.Message{}, err - } - - select { - case response := <-responseCh: - return response, nil - case <-ctx.Done(): - return websocket.Message{}, ctx.Err() + return err } + return rs.wait(ctx) } type CallServiceRequest struct { @@ -68,12 +51,14 @@ type CallServiceRequest struct { } // CallService invokes a service using a `call_service` message, then -// waits for and returns the response. -// -// FIXME: can the response be parsed into a result-style message? +// waits for the response. The response is evaluated; if it indicates +// an error, then this method returns that error. Otherwise, the +// "result" field is stored to `result`, which must be something that +// `json.Unmarshal()` can serialize into (typically a pointer). func (app *App) CallService( ctx context.Context, domain string, service string, serviceData any, target ga.Target, -) (websocket.Message, error) { + result any, +) error { req := CallServiceRequest{ BaseMessage: websocket.BaseMessage{ Type: "call_service", @@ -84,7 +69,15 @@ func (app *App) CallService( Target: target, } - return app.Call(ctx, &req) + if err := app.Call(ctx, &req, result); err != nil { + switch target { + case ga.Target{}: + return fmt.Errorf("calling '%s.%s': %w", domain, service, err) + default: + return fmt.Errorf("calling '%s.%s' for %s: %w", domain, service, target, err) + } + } + return nil } // Subscribe subscribes to some events via `req`, waits for a single @@ -102,28 +95,40 @@ func (app *App) CallService( // cleanup to the caller. func (app *App) Subscribe( ctx context.Context, req websocket.Request, subscriber websocket.Subscriber, -) (websocket.Message, websocket.Subscription, error) { +) (websocket.ResultMessage, websocket.Subscription, error) { // The result of the attempt to subscribe (i.e., the first // message) will be sent to this channel. resultReceived := false - resultCh := make(chan websocket.Message, 1) + var resultMsg websocket.ResultMessage + var resultErr error + done := make(chan struct{}) var subscription websocket.Subscription - // Receive a single message, sent it to `responseCh`, then - // unsubscribe: + // Receive a single "result" message, send it to `responseCh`, + // then unsubscribe: dualSubscriber := func(msg websocket.Message) { - if !resultReceived { - // This is the first message. We send it to the channel so - // that it can be returned from the outer function. - defer close(resultCh) - resultCh <- msg + if msg.Type == "result" { + if resultReceived { + slog.Warn( + "Error: multiple responses received for one 'subscribe' request (ignored)", + ) + return + } resultReceived = true + + defer close(done) + + resultErr = json.Unmarshal(msg.Raw, &resultMsg) + if resultErr != nil { + return + } + // FIXME: turn non-success responses into errors. return } - // The result has already been processed. Subsequent events - // get forwarded to `subscriber`: + // Forward other responses (i.e., the events themselves) to + // `subscriber`: subscriber(msg) } @@ -138,13 +143,14 @@ func (app *App) Subscribe( }) if err != nil { - return websocket.Message{}, websocket.Subscription{}, err + return websocket.ResultMessage{}, websocket.Subscription{}, err } select { - case response := <-resultCh: - return response, subscription, nil + case <-done: + return resultMsg, subscription, nil case <-ctx.Done(): - return websocket.Message{}, websocket.Subscription{}, ctx.Err() + // FIXME: unsubscribe + return websocket.ResultMessage{}, websocket.Subscription{}, ctx.Err() } } diff --git a/app/result_subscriber.go b/app/result_subscriber.go new file mode 100644 index 0000000..e3513fd --- /dev/null +++ b/app/result_subscriber.go @@ -0,0 +1,82 @@ +package app + +import ( + "context" + "fmt" + "log/slog" + "sync" + + "saml.dev/gome-assistant/websocket" +) + +// resultSubscriber is a helper type for handling the result message +// sent by the server in response to some kind of request. It +// subscribes itself, sends a message to the server, captures the +// first `result` message, then unsubscribes itself. The +// `ResultMessage` or error can be read using `wait()`. +type resultSubscriber struct { + app *App + subscription websocket.Subscription + + once sync.Once + result any + err error + done chan struct{} +} + +// newResultSubscriber creates a new subscriber that writes its result +// into `result`, which must be something that `json.Unmarshal()` can +// marshal into (typically a pointer). +func newResultSubscriber(app *App, result any) *resultSubscriber { + return &resultSubscriber{ + app: app, + result: result, + done: make(chan struct{}), + } +} + +// subscribe prepares and sends `req` to `lc`, but first subscribes +// `rs.callback` to receive the result of the request. +func (rs *resultSubscriber) subscribe( + lc websocket.LockedConn, req websocket.Request, +) error { + rs.subscription = lc.Subscribe(rs.callback) + req.SetID(rs.subscription.ID()) + if err := lc.SendMessage(req); err != nil { + lc.Unsubscribe(rs.subscription) + return fmt.Errorf("error writing to websocket: %w", err) + } + return nil +} + +// callback receives a single "result" message, stores the result to +// `rs`, then unsubscribes. It implements `websocket.Subscriber`. +func (rs *resultSubscriber) callback(msg websocket.Message) { + defer rs.close() + rs.err = msg.GetResult(rs.result) +} + +// wait waits for the result message to be received by `callback()`, +// then returns it to the caller. +func (rs *resultSubscriber) wait(ctx context.Context) error { + select { + case <-rs.done: + return rs.err + case <-ctx.Done(): + rs.close() + return ctx.Err() + } +} + +func (rs *resultSubscriber) close() { + rs.once.Do(func() { + close(rs.done) + err := rs.app.wsConn.Send(func(lc websocket.LockedConn) error { + lc.Unsubscribe(rs.subscription) + return nil + }) + if err != nil { + slog.Warn("Error unsubscribing", "message_id", rs.subscription.ID()) + } + }) +} diff --git a/internal/services/alarm_control_panel.go b/internal/services/alarm_control_panel.go index 3d7fb10..4406ba3 100644 --- a/internal/services/alarm_control_panel.go +++ b/internal/services/alarm_control_panel.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,14 +21,17 @@ func NewAlarmControlPanel(service Service) *AlarmControlPanel { } // Send the alarm the command for arm away. -func (acp AlarmControlPanel) ArmAway( - target ga.Target, serviceData any, -) (websocket.Message, error) { +func (acp AlarmControlPanel) ArmAway(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return acp.service.CallService( + var result any + err := acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_away", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the alarm the command for arm away. @@ -37,67 +39,87 @@ func (acp AlarmControlPanel) ArmAway( // map that is translated into service_data. func (acp AlarmControlPanel) ArmWithCustomBypass( target ga.Target, serviceData any, -) (websocket.Message, error) { +) (any, error) { ctx := context.TODO() - return acp.service.CallService( + var result any + err := acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_custom_bypass", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // 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( - target ga.Target, serviceData any, -) (websocket.Message, error) { +func (acp AlarmControlPanel) ArmHome(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return acp.service.CallService( + var result any + err := acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_home", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the alarm the command for arm night. -func (acp AlarmControlPanel) ArmNight( - target ga.Target, serviceData any, -) (websocket.Message, error) { +func (acp AlarmControlPanel) ArmNight(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return acp.service.CallService( + var result any + err := acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_night", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the alarm the command for arm vacation. -func (acp AlarmControlPanel) ArmVacation( - target ga.Target, serviceData any, -) (websocket.Message, error) { +func (acp AlarmControlPanel) ArmVacation(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return acp.service.CallService( + var result any + err := acp.service.CallService( ctx, "alarm_control_panel", "alarm_arm_vacation", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the alarm the command for disarm. -func (acp AlarmControlPanel) Disarm( - target ga.Target, serviceData any, -) (websocket.Message, error) { +func (acp AlarmControlPanel) Disarm(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return acp.service.CallService( + var result any + err := acp.service.CallService( ctx, "alarm_control_panel", "alarm_disarm", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the alarm the command for trigger. -func (acp AlarmControlPanel) Trigger( - target ga.Target, serviceData any, -) (websocket.Message, error) { +func (acp AlarmControlPanel) Trigger(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return acp.service.CallService( + var result any + err := acp.service.CallService( ctx, "alarm_control_panel", "alarm_trigger", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/climate.go b/internal/services/climate.go index 6522503..2cf37f3 100644 --- a/internal/services/climate.go +++ b/internal/services/climate.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -19,15 +18,18 @@ func NewClimate(service Service) *Climate { } } -func (c Climate) SetFanMode( - target ga.Target, fanMode string, -) (websocket.Message, error) { +func (c Climate) SetFanMode(target ga.Target, fanMode string) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "climate", "set_fan_mode", map[string]any{"fan_mode": fanMode}, - target, + target, &result, ) + if err != nil { + return nil, err + } + return result, nil } type SetTemperatureRequest struct { @@ -56,11 +58,16 @@ func (r *SetTemperatureRequest) ToJSON() map[string]any { func (c Climate) SetTemperature( target ga.Target, setTemperatureRequest SetTemperatureRequest, -) (websocket.Message, error) { +) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "climate", "set_temperature", setTemperatureRequest.ToJSON(), - target, + target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/cover.go b/internal/services/cover.go index e72e58c..9ac0569 100644 --- a/internal/services/cover.go +++ b/internal/services/cover.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,93 +21,143 @@ func NewCover(service Service) *Cover { /* Public API */ // Close all or specified cover. Takes an entityID. -func (c Cover) Close(target ga.Target) (websocket.Message, error) { +func (c Cover) Close(target ga.Target) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "close_cover", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Close all or specified cover tilt. Takes an entityID. -func (c Cover) CloseTilt(target ga.Target) (websocket.Message, error) { +func (c Cover) CloseTilt(target ga.Target) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "close_cover_tilt", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Open all or specified cover. Takes an entityID. -func (c Cover) Open(target ga.Target) (websocket.Message, error) { +func (c Cover) Open(target ga.Target) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "open_cover", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Open all or specified cover tilt. Takes an entityID. -func (c Cover) OpenTilt(target ga.Target) (websocket.Message, error) { +func (c Cover) OpenTilt(target ga.Target) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "open_cover_tilt", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // 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(target ga.Target, serviceData any) (websocket.Message, error) { +func (c Cover) SetPosition(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "set_cover_position", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // 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(target ga.Target, serviceData any) (websocket.Message, error) { +func (c Cover) SetTiltPosition(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "set_cover_tilt_position", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Stop a cover entity. Takes an entityID. -func (c Cover) Stop(target ga.Target) (websocket.Message, error) { +func (c Cover) Stop(target ga.Target) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "stop_cover", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Stop a cover entity tilt. Takes an entityID. -func (c Cover) StopTilt(target ga.Target) (websocket.Message, error) { +func (c Cover) StopTilt(target ga.Target) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "stop_cover_tilt", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Toggle a cover open/closed. Takes an entityID. -func (c Cover) Toggle(target ga.Target) (websocket.Message, error) { +func (c Cover) Toggle(target ga.Target) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "toggle", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Toggle a cover tilt open/closed. Takes an entityID. -func (c Cover) ToggleTilt(target ga.Target) (websocket.Message, error) { +func (c Cover) ToggleTilt(target ga.Target) (any, error) { ctx := context.TODO() - return c.service.CallService( + var result any + err := c.service.CallService( ctx, "cover", "toggle_cover_tilt", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/event.go b/internal/services/event.go index ead6c42..3c50d41 100644 --- a/internal/services/event.go +++ b/internal/services/event.go @@ -27,7 +27,7 @@ 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) (websocket.Message, error) { +func (e Event) Fire(eventType string, eventData map[string]any) error { ctx := context.TODO() req := FireEventRequest{ @@ -39,5 +39,6 @@ func (e Event) Fire(eventType string, eventData map[string]any) (websocket.Messa req.EventType = eventType req.EventData = eventData - return e.service.Call(ctx, &req) + var result any + return e.service.Call(ctx, &req, &result) } diff --git a/internal/services/homeassistant.go b/internal/services/homeassistant.go index 05093e9..d683433 100644 --- a/internal/services/homeassistant.go +++ b/internal/services/homeassistant.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) type HomeAssistant struct { @@ -19,28 +18,43 @@ func NewHomeAssistant(service Service) *HomeAssistant { // TurnOn a Home Assistant entity. Takes an entityID and an optional // map that is translated into service_data. -func (ha *HomeAssistant) TurnOn(target ga.Target, serviceData any) (websocket.Message, error) { +func (ha *HomeAssistant) TurnOn(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return ha.service.CallService( + var result any + err := ha.service.CallService( ctx, "homeassistant", "turn_on", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Toggle a Home Assistant entity. Takes an entityID and an optional // map that is translated into service_data. -func (ha *HomeAssistant) Toggle(target ga.Target, serviceData any) (websocket.Message, error) { +func (ha *HomeAssistant) Toggle(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return ha.service.CallService( + var result any + err := ha.service.CallService( ctx, "homeassistant", "toggle", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ha *HomeAssistant) TurnOff(target ga.Target) (websocket.Message, error) { +func (ha *HomeAssistant) TurnOff(target ga.Target) (any, error) { ctx := context.TODO() - return ha.service.CallService( + var result any + err := ha.service.CallService( ctx, "homeassistant", "turn_off", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/input_boolean.go b/internal/services/input_boolean.go index 2a7eadd..62884df 100644 --- a/internal/services/input_boolean.go +++ b/internal/services/input_boolean.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -21,33 +20,53 @@ func NewInputBoolean(service Service) *InputBoolean { /* Public API */ -func (ib InputBoolean) TurnOn(target ga.Target) (websocket.Message, error) { +func (ib InputBoolean) TurnOn(target ga.Target) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_boolean", "turn_on", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputBoolean) Toggle(target ga.Target) (websocket.Message, error) { +func (ib InputBoolean) Toggle(target ga.Target) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_boolean", "toggle", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputBoolean) TurnOff(target ga.Target) (websocket.Message, error) { +func (ib InputBoolean) TurnOff(target ga.Target) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_boolean", "turn_off", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputBoolean) Reload() (websocket.Message, error) { +func (ib InputBoolean) Reload() (any, error) { ctx := context.TODO() - return ib.service.CallService( - ctx, "input_boolean", "reload", nil, ga.Target{}, + var result any + err := ib.service.CallService( + ctx, "input_boolean", "reload", nil, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/input_button.go b/internal/services/input_button.go index 1152953..45c93b1 100644 --- a/internal/services/input_button.go +++ b/internal/services/input_button.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -21,17 +20,27 @@ func NewInputButton(service Service) *InputButton { /* Public API */ -func (ib InputButton) Press(target ga.Target) (websocket.Message, error) { +func (ib InputButton) Press(target ga.Target) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_button", "press", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputButton) Reload() (websocket.Message, error) { +func (ib InputButton) Reload() (any, error) { ctx := context.TODO() - return ib.service.CallService( - ctx, "input_button", "reload", nil, ga.Target{}, + var result any + err := ib.service.CallService( + ctx, "input_button", "reload", nil, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/input_datetime.go b/internal/services/input_datetime.go index cf60be2..f206fdb 100644 --- a/internal/services/input_datetime.go +++ b/internal/services/input_datetime.go @@ -6,7 +6,6 @@ import ( "time" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -23,20 +22,30 @@ func NewInputDatetime(service Service) *InputDatetime { /* Public API */ -func (ib InputDatetime) Set(target ga.Target, value time.Time) (websocket.Message, error) { +func (ib InputDatetime) Set(target ga.Target, value time.Time) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_datetime", "set_datetime", map[string]any{ "timestamp": fmt.Sprint(value.Unix()), }, - target, + target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputDatetime) Reload() (websocket.Message, error) { +func (ib InputDatetime) Reload() (any, error) { ctx := context.TODO() - return ib.service.CallService( - ctx, "input_datetime", "reload", nil, ga.Target{}, + var result any + err := ib.service.CallService( + ctx, "input_datetime", "reload", nil, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/input_number.go b/internal/services/input_number.go index bd36dd9..b803960 100644 --- a/internal/services/input_number.go +++ b/internal/services/input_number.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -21,34 +20,54 @@ func NewInputNumber(service Service) *InputNumber { /* Public API */ -func (ib InputNumber) Set(target ga.Target, value float32) (websocket.Message, error) { +func (ib InputNumber) Set(target ga.Target, value float32) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_number", "set_value", map[string]any{"value": value}, - target, + target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputNumber) Increment(target ga.Target) (websocket.Message, error) { +func (ib InputNumber) Increment(target ga.Target) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_number", "increment", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputNumber) Decrement(target ga.Target) (websocket.Message, error) { +func (ib InputNumber) Decrement(target ga.Target) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_number", "decrement", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputNumber) Reload() (websocket.Message, error) { +func (ib InputNumber) Reload() (any, error) { ctx := context.TODO() - return ib.service.CallService( - ctx, "input_number", "reload", nil, ga.Target{}, + var result any + err := ib.service.CallService( + ctx, "input_number", "reload", nil, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/input_text.go b/internal/services/input_text.go index 8d5e832..26e0f76 100644 --- a/internal/services/input_text.go +++ b/internal/services/input_text.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -21,20 +20,30 @@ func NewInputText(service Service) *InputText { /* Public API */ -func (ib InputText) Set(target ga.Target, value string) (websocket.Message, error) { +func (ib InputText) Set(target ga.Target, value string) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "input_text", "set_value", map[string]any{ "value": value, }, - target, + target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (ib InputText) Reload() (websocket.Message, error) { +func (ib InputText) Reload() (any, error) { ctx := context.TODO() - return ib.service.CallService( - ctx, "input_text", "reload", nil, ga.Target{}, + var result any + err := ib.service.CallService( + ctx, "input_text", "reload", nil, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/light.go b/internal/services/light.go index cbc3bb9..ecd7576 100644 --- a/internal/services/light.go +++ b/internal/services/light.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,24 +21,39 @@ func NewLight(service Service) *Light { /* Public API */ // TurnOn a light entity. -func (l Light) TurnOn(target ga.Target, serviceData any) (websocket.Message, error) { +func (l Light) TurnOn(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return l.service.CallService( - ctx, "light", "turn_on", serviceData, target, + var result any + err := l.service.CallService( + ctx, "light", "turn_on", serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Toggle a light entity. -func (l Light) Toggle(target ga.Target, serviceData any) (websocket.Message, error) { +func (l Light) Toggle(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return l.service.CallService( - ctx, "light", "toggle", serviceData, target, + var result any + err := l.service.CallService( + ctx, "light", "toggle", serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (l Light) TurnOff(target ga.Target) (websocket.Message, error) { +func (l Light) TurnOff(target ga.Target) (any, error) { ctx := context.TODO() - return l.service.CallService( - ctx, "light", "turn_off", nil, target, + var result any + err := l.service.CallService( + ctx, "light", "turn_off", nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/lock.go b/internal/services/lock.go index 17871d9..086f49b 100644 --- a/internal/services/lock.go +++ b/internal/services/lock.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,19 +21,29 @@ func NewLock(service Service) *Lock { /* Public API */ // Lock a lock entity. -func (l Lock) Lock(target ga.Target, serviceData any) (websocket.Message, error) { +func (l Lock) Lock(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return l.service.CallService( + var result any + err := l.service.CallService( ctx, "lock", "lock", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Unlock a lock entity. -func (l Lock) Unlock(target ga.Target, serviceData any) (websocket.Message, error) { +func (l Lock) Unlock(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return l.service.CallService( + var result any + err := l.service.CallService( ctx, "lock", "unlock", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/media_player.go b/internal/services/media_player.go index 660dc48..ed6eb53 100644 --- a/internal/services/media_player.go +++ b/internal/services/media_player.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,204 +21,310 @@ func NewMediaPlayer(service Service) *MediaPlayer { /* Public API */ // Send the media player the command to clear players playlist. -func (mp MediaPlayer) ClearPlaylist(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) ClearPlaylist(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "clear_playlist", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Group players together. Only works on platforms with support for player groups. -func (mp MediaPlayer) Join(target ga.Target, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) Join(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "join", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the command for next track. -func (mp MediaPlayer) Next(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) Next(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "media_next_track", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the command for pause. -func (mp MediaPlayer) Pause(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) Pause(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "media_pause", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the command for play. -func (mp MediaPlayer) Play(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) Play(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "media_play", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Toggle media player play/pause state. -func (mp MediaPlayer) PlayPause(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) PlayPause(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "media_play_pause", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the command for previous track. -func (mp MediaPlayer) Previous(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) Previous(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "media_previous_track", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the command to seek in current playing media. -func (mp MediaPlayer) Seek(target ga.Target, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) Seek(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "media_seek", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the stop command. -func (mp MediaPlayer) Stop(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) Stop(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "media_stop", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the command for playing media. -func (mp MediaPlayer) PlayMedia(target ga.Target, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) PlayMedia(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "play_media", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Set repeat mode. -func (mp MediaPlayer) RepeatSet(target ga.Target, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) RepeatSet(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "repeat_set", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the command to change sound mode. -func (mp MediaPlayer) SelectSoundMode( - target ga.Target, serviceData any, -) (websocket.Message, error) { +func (mp MediaPlayer) SelectSoundMode(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "select_sound_mode", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send the media player the command to change input source. -func (mp MediaPlayer) SelectSource( - target ga.Target, serviceData any, -) (websocket.Message, error) { +func (mp MediaPlayer) SelectSource(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "select_source", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Set shuffling state. -func (mp MediaPlayer) Shuffle(target ga.Target, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) Shuffle(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "shuffle_set", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Toggles a media player power state. -func (mp MediaPlayer) Toggle(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) Toggle(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "toggle", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Turn a media player power off. -func (mp MediaPlayer) TurnOff(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) TurnOff(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "turn_off", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Turn a media player power on. -func (mp MediaPlayer) TurnOn(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) TurnOn(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "turn_on", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Unjoin the player from a group. Only works on // platforms with support for player groups. -func (mp MediaPlayer) Unjoin(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) Unjoin(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "unjoin", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Turn a media player volume down. -func (mp MediaPlayer) VolumeDown(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) VolumeDown(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "volume_down", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Mute a media player's volume. -func (mp MediaPlayer) VolumeMute(target ga.Target, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) VolumeMute(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "volume_mute", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Set a media player's volume level. -func (mp MediaPlayer) VolumeSet(target ga.Target, serviceData any) (websocket.Message, error) { +func (mp MediaPlayer) VolumeSet(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "volume_set", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Turn a media player volume up. -func (mp MediaPlayer) VolumeUp(target ga.Target) (websocket.Message, error) { +func (mp MediaPlayer) VolumeUp(target ga.Target) (any, error) { ctx := context.TODO() - return mp.service.CallService( + var result any + err := mp.service.CallService( ctx, "media_player", "volume_up", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/notify.go b/internal/services/notify.go index 17bc41a..73510eb 100644 --- a/internal/services/notify.go +++ b/internal/services/notify.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) type Notify struct { @@ -26,7 +25,7 @@ type NotifyRequest struct { } // Send a notification. -func (ha *Notify) Notify(reqData NotifyRequest) (websocket.Message, error) { +func (ha *Notify) Notify(reqData NotifyRequest) (any, error) { ctx := context.TODO() serviceData := map[string]any{ "message": reqData.Message, @@ -36,8 +35,13 @@ func (ha *Notify) Notify(reqData NotifyRequest) (websocket.Message, error) { serviceData["data"] = reqData.Data } - return ha.service.CallService( + var result any + err := ha.service.CallService( ctx, "notify", reqData.ServiceName, - serviceData, ga.Target{}, + serviceData, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/number.go b/internal/services/number.go index 03fd5e4..f282620 100644 --- a/internal/services/number.go +++ b/internal/services/number.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -21,11 +20,16 @@ func NewNumber(service Service) *Number { /* Public API */ -func (ib Number) SetValue(target ga.Target, value float32) (websocket.Message, error) { +func (ib Number) SetValue(target ga.Target, value float32) (any, error) { ctx := context.TODO() - return ib.service.CallService( + var result any + err := ib.service.CallService( ctx, "number", "set_value", map[string]any{"value": value}, - target, + target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/scene.go b/internal/services/scene.go index 7af643c..d37a674 100644 --- a/internal/services/scene.go +++ b/internal/services/scene.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,36 +21,56 @@ func NewScene(service Service) *Scene { /* Public API */ // Apply a scene. Takes map that is translated into service_data. -func (s Scene) Apply(serviceData any) (websocket.Message, error) { +func (s Scene) Apply(serviceData any) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "scene", "apply", - serviceData, ga.Target{}, + serviceData, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Create a scene entity. -func (s Scene) Create(target ga.Target, serviceData any) (websocket.Message, error) { +func (s Scene) Create(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "scene", "create", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Reload the scenes. -func (s Scene) Reload() (websocket.Message, error) { +func (s Scene) Reload() (any, error) { ctx := context.TODO() - return s.service.CallService( - ctx, "scene", "reload", nil, ga.Target{}, + var result any + err := s.service.CallService( + ctx, "scene", "reload", nil, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } // TurnOn a scene entity. -func (s Scene) TurnOn(target ga.Target, serviceData any) (websocket.Message, error) { +func (s Scene) TurnOn(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "scene", "turn_on", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/script.go b/internal/services/script.go index 6217cc2..7a63149 100644 --- a/internal/services/script.go +++ b/internal/services/script.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,37 +21,57 @@ func NewScript(service Service) *Script { /* Public API */ // Reload a script that was created in the HA UI. -func (s Script) Reload(target ga.Target) (websocket.Message, error) { +func (s Script) Reload(target ga.Target) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "script", "reload", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Toggle a script that was created in the HA UI. -func (s Script) Toggle(target ga.Target) (websocket.Message, error) { +func (s Script) Toggle(target ga.Target) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "script", "toggle", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Turn off a script that was created in the HA UI. -func (s Script) TurnOff() (websocket.Message, error) { +func (s Script) TurnOff() (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "script", "turn_off", - nil, ga.Target{}, + nil, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Turn on a script that was created in the HA UI. -func (s Script) TurnOn(target ga.Target) (websocket.Message, error) { +func (s Script) TurnOn(target ga.Target) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "script", "turn_on", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/services.go b/internal/services/services.go index 8706b39..2052ea1 100644 --- a/internal/services/services.go +++ b/internal/services/services.go @@ -9,10 +9,11 @@ import ( type Service interface { Call( - ctx context.Context, req websocket.Request, - ) (websocket.Message, error) + ctx context.Context, req websocket.Request, result any, + ) error CallService( ctx context.Context, domain string, service string, serviceData any, target ga.Target, - ) (websocket.Message, error) + result any, + ) error } diff --git a/internal/services/switch.go b/internal/services/switch.go index 2d23ff6..65bc593 100644 --- a/internal/services/switch.go +++ b/internal/services/switch.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -21,26 +20,41 @@ func NewSwitch(service Service) *Switch { /* Public API */ -func (s Switch) TurnOn(target ga.Target) (websocket.Message, error) { +func (s Switch) TurnOn(target ga.Target) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "switch", "turn_on", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (s Switch) Toggle(target ga.Target) (websocket.Message, error) { +func (s Switch) Toggle(target ga.Target) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "switch", "toggle", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } -func (s Switch) TurnOff(target ga.Target) (websocket.Message, error) { +func (s Switch) TurnOff(target ga.Target) (any, error) { ctx := context.TODO() - return s.service.CallService( + var result any + err := s.service.CallService( ctx, "switch", "turn_off", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/tts.go b/internal/services/tts.go index 71d1c1e..a05a7c2 100644 --- a/internal/services/tts.go +++ b/internal/services/tts.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,28 +21,43 @@ func NewTTS(service Service) *TTS { /* Public API */ // Remove all text-to-speech cache files and RAM cache. -func (tts TTS) ClearCache() (websocket.Message, error) { +func (tts TTS) ClearCache() (any, error) { ctx := context.TODO() - return tts.service.CallService( - ctx, "tts", "clear_cache", nil, ga.Target{}, + var result any + err := tts.service.CallService( + ctx, "tts", "clear_cache", nil, ga.Target{}, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Say something using text-to-speech on a media player with cloud. -func (tts TTS) CloudSay(target ga.Target, serviceData any) (websocket.Message, error) { +func (tts TTS) CloudSay(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return tts.service.CallService( + var result any + err := tts.service.CallService( ctx, "tts", "cloud_say", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Say something using text-to-speech on a media player with // google_translate. -func (tts TTS) GoogleTranslateSay(target ga.Target, serviceData any) (websocket.Message, error) { +func (tts TTS) GoogleTranslateSay(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return tts.service.CallService( + var result any + err := tts.service.CallService( ctx, "tts", "google_translate_say", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/vacuum.go b/internal/services/vacuum.go index 3b47229..0dda3db 100644 --- a/internal/services/vacuum.go +++ b/internal/services/vacuum.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -22,100 +21,155 @@ func NewVacuum(service Service) *Vacuum { /* Public API */ // Tell the vacuum cleaner to do a spot clean-up. -func (v Vacuum) CleanSpot(target ga.Target) (websocket.Message, error) { +func (v Vacuum) CleanSpot(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "clean_spot", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Locate the vacuum cleaner robot. -func (v Vacuum) Locate(target ga.Target) (websocket.Message, error) { +func (v Vacuum) Locate(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "locate", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Pause the cleaning task. -func (v Vacuum) Pause(target ga.Target) (websocket.Message, error) { +func (v Vacuum) Pause(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "pause", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Tell the vacuum cleaner to return to its dock. -func (v Vacuum) ReturnToBase(target ga.Target) (websocket.Message, error) { +func (v Vacuum) ReturnToBase(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "return_to_base", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Send a raw command to the vacuum cleaner. -func (v Vacuum) SendCommand(target ga.Target, serviceData any) (websocket.Message, error) { +func (v Vacuum) SendCommand(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "send_command", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Set the fan speed of the vacuum cleaner. -func (v Vacuum) SetFanSpeed(target ga.Target, serviceData any) (websocket.Message, error) { +func (v Vacuum) SetFanSpeed(target ga.Target, serviceData any) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "set_fan_speed", - serviceData, target, + serviceData, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Start or resume the cleaning task. -func (v Vacuum) Start(target ga.Target) (websocket.Message, error) { +func (v Vacuum) Start(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "start", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Start, pause, or resume the cleaning task. -func (v Vacuum) StartPause(target ga.Target) (websocket.Message, error) { +func (v Vacuum) StartPause(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "start_pause", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Stop the current cleaning task. -func (v Vacuum) Stop(target ga.Target) (websocket.Message, error) { +func (v Vacuum) Stop(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "stop", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Stop the current cleaning task and return to home. -func (v Vacuum) TurnOff(target ga.Target) (websocket.Message, error) { +func (v Vacuum) TurnOff(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "turn_off", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } // Start a new cleaning task. -func (v Vacuum) TurnOn(target ga.Target) (websocket.Message, error) { +func (v Vacuum) TurnOn(target ga.Target) (any, error) { ctx := context.TODO() - return v.service.CallService( + var result any + err := v.service.CallService( ctx, "vacuum", "turn_on", - nil, target, + nil, target, &result, ) + if err != nil { + return nil, err + } + return result, nil } diff --git a/internal/services/zwavejs.go b/internal/services/zwavejs.go index c5c894c..ec8d701 100644 --- a/internal/services/zwavejs.go +++ b/internal/services/zwavejs.go @@ -4,7 +4,6 @@ import ( "context" ga "saml.dev/gome-assistant" - "saml.dev/gome-assistant/websocket" ) /* Structs */ @@ -24,14 +23,19 @@ func NewZWaveJS(service Service) *ZWaveJS { // ZWaveJS bulk_set_partial_config_parameters service. func (zw ZWaveJS) BulkSetPartialConfigParam( target ga.Target, parameter int, value any, -) (websocket.Message, error) { +) (any, error) { ctx := context.TODO() - return zw.service.CallService( + var result any + err := zw.service.CallService( ctx, "zwave_js", "bulk_set_partial_config_parameters", map[string]any{ "parameter": parameter, "value": value, }, - target, + target, &result, ) + if err != nil { + return nil, err + } + return result, nil } From 162e969b904860b3af14f2349ba80a7e3f5666c4 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Wed, 15 May 2024 10:51:54 +0200 Subject: [PATCH 086/103] Start adding code to handle websocket `state_changed` events --- websocket/message.go | 152 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) diff --git a/websocket/message.go b/websocket/message.go index 3029a42..c6a3e31 100644 --- a/websocket/message.go +++ b/websocket/message.go @@ -3,12 +3,20 @@ package websocket import ( "encoding/json" "fmt" + "log/slog" ) // RawMessage is like `json.RawMessage`, except that its `String()` // method converts it directly to a string. type RawMessage json.RawMessage +func (m RawMessage) MarshalJSON() ([]byte, error) { + if m == nil { + return []byte("null"), nil + } + return m, nil +} + // UnmarshalJSON delegates to `json.RawMessage`. (The method has a // pointer receiver, so we have to implement it explicitly.) func (m *RawMessage) UnmarshalJSON(data []byte) error { @@ -109,3 +117,147 @@ func (msg Message) GetResult(result any) error { } return nil } + +// "state_changed" events are compressed in a rather awkward way. +// These types help pick them apart. + +type EntityState struct { + State RawMessage `json:"state"` + Attributes map[string]RawMessage `json:"attributes"` + Context RawMessage `json:"context"` + LastChanged RawMessage `json:"last_changed"` +} + +type EntityStateItem struct { + EntityID string `json:"entity_id"` + EntityState +} + +// CompressedEntityState is similar to `EntityState` except that it +// doesn't include the entity ID and the JSON field names are +// abbreviated. +type CompressedEntityState struct { + State RawMessage `json:"s"` + Attributes map[string]RawMessage `json:"a"` + Context RawMessage `json:"c"` + LastChanged RawMessage `json:"lc"` +} + +// CompressedEntityChange keeps tracks of fields added and removed as +// part of a change. Fields that are mutated appear as "additions". +type CompressedEntityChange struct { + Additions CompressedEntityState `json:"+,omitempty"` + Removals struct { + Attributes []string `json:"a"` + Context []string `json:"c"` + } `json:"-,omitempty"` +} + +type CompressedStateChangedMessage struct { + BaseMessage + Event struct { + Added map[string]CompressedEntityState `json:"a,omitempty"` + Changed map[string]CompressedEntityChange `json:"c,omitempty"` + Removed []string `json:"r,omitempty"` + } `json:"event"` +} + +// Apply applies the changes indicated in `msg` to the entity with the +// specified `entityID` whose old state was `oldState`, returning the +// new state. +func (msg CompressedStateChangedMessage) Apply( + entityID string, oldState EntityState, +) (EntityState, error) { + if state, ok := msg.Event.Added[entityID]; ok { + // This entityID was added. The new state was right there in + // the message. + return EntityState(state), nil + } + if change, ok := msg.Event.Changed[entityID]; ok { + state := oldState.State + if len(change.Additions.State) != 0 { + state = change.Additions.State + } + // The existing entry has had some fields changed. + return EntityState{ + State: state, + Attributes: mergeMaps( + oldState.Attributes, + change.Additions.Attributes, + change.Removals.Attributes, + ), + // FIXME: apparently, context can also be a single string. + Context: mergeContexts( + oldState.Context, + change.Additions.Context, + change.Removals.Context, + ), + LastChanged: change.Additions.LastChanged, + }, nil + } + for _, eid := range msg.Event.Removed { + if eid == entityID { + return EntityState{}, nil + } + } + return oldState, nil +} + +func mergeMaps(old, additions map[string]RawMessage, removals []string) map[string]RawMessage { + new := make(map[string]RawMessage, len(old)+len(additions)-len(removals)) + for k, v := range old { + new[k] = v + } + for k, v := range additions { + new[k] = v + } + for _, k := range removals { + delete(new, k) + } + return new +} + +func mergeContexts(old, additions RawMessage, removals []string) RawMessage { + switch { + case len(old) == 0: + return additions + case old[0] == '"': + // The context is a single string. + if len(additions) != 0 { + return additions + } + return old + case old[0] == '{': + // The context is an object. + var contextMap map[string]RawMessage + if err := json.Unmarshal(old, &contextMap); err != nil { + slog.Error("cannot unmarshal old context", + "old", string(old), + "error", err, + ) + return additions + } + var addMap map[string]RawMessage + if len(additions) != 0 { + if err := json.Unmarshal(additions, &addMap); err != nil { + slog.Error("cannot unmarshal additions", + "additions", string(additions), + "error", err, + ) + return old + } + } + newMap := mergeMaps(contextMap, addMap, removals) + newContext, err := json.Marshal(newMap) + if err != nil { + slog.Error("cannot marshal new context", + "context", newMap, + "error", err, + ) + return old + } + return newContext + default: + return old + } +} From cf22ace51458740519fb4a967116aefd362f9676 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 26 May 2024 15:33:11 +0200 Subject: [PATCH 087/103] Move `RawMessage` definition to a separate file --- websocket/message.go | 21 --------------------- websocket/raw_message.go | 26 ++++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 21 deletions(-) create mode 100644 websocket/raw_message.go diff --git a/websocket/message.go b/websocket/message.go index c6a3e31..3177825 100644 --- a/websocket/message.go +++ b/websocket/message.go @@ -6,27 +6,6 @@ import ( "log/slog" ) -// RawMessage is like `json.RawMessage`, except that its `String()` -// method converts it directly to a string. -type RawMessage json.RawMessage - -func (m RawMessage) MarshalJSON() ([]byte, error) { - if m == nil { - return []byte("null"), nil - } - return m, nil -} - -// UnmarshalJSON delegates to `json.RawMessage`. (The method has a -// pointer receiver, so we have to implement it explicitly.) -func (m *RawMessage) UnmarshalJSON(data []byte) error { - return (*json.RawMessage)(m).UnmarshalJSON(data) -} - -func (rm RawMessage) String() string { - return string(rm) -} - // BaseMessage implements the required part of any websocket message. // The idea is to embed this type in other message types. type BaseMessage struct { diff --git a/websocket/raw_message.go b/websocket/raw_message.go new file mode 100644 index 0000000..ad4ee22 --- /dev/null +++ b/websocket/raw_message.go @@ -0,0 +1,26 @@ +package websocket + +import ( + "encoding/json" +) + +// RawMessage is like `json.RawMessage`, except that its `String()` +// method converts it directly to a string. +type RawMessage json.RawMessage + +func (m RawMessage) MarshalJSON() ([]byte, error) { + if m == nil { + return []byte("null"), nil + } + return m, nil +} + +// UnmarshalJSON delegates to `json.RawMessage`. (The method has a +// pointer receiver, so we have to implement it explicitly.) +func (m *RawMessage) UnmarshalJSON(data []byte) error { + return (*json.RawMessage)(m).UnmarshalJSON(data) +} + +func (rm RawMessage) String() string { + return string(rm) +} From e04a1b98c01a6d4e6538bd09ee8931727cf14727 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 26 May 2024 15:35:09 +0200 Subject: [PATCH 088/103] Move `ResultMessage` definition to a separate file --- websocket/message.go | 62 ---------------------------------- websocket/result_message.go | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 62 deletions(-) create mode 100644 websocket/result_message.go diff --git a/websocket/message.go b/websocket/message.go index 3177825..9dfa681 100644 --- a/websocket/message.go +++ b/websocket/message.go @@ -2,7 +2,6 @@ package websocket import ( "encoding/json" - "fmt" "log/slog" ) @@ -36,67 +35,6 @@ type Message struct { Raw RawMessage `json:"-"` } -type BaseResultMessage struct { - BaseMessage - Success bool `json:"success"` - Error *ResultError `json:"error,omitempty"` -} - -type ResultError struct { - Code string `json:"code"` - Message string `json:"message"` -} - -func (err *ResultError) Error() string { - switch { - case err.Code != "" && err.Message != "": - return fmt.Sprintf("%s: %s", err.Code, err.Message) - case err.Code == "" && err.Message != "": - return fmt.Sprintf("unknown_error: %s", err.Message) - case err.Code != "" && err.Message == "": - return fmt.Sprintf("%s", err.Code) - default: - // This seems not to be an error at all. - return fmt.Sprintf("INVALID (seems not to be an error)") - } -} - -type ResultMessage struct { - BaseResultMessage - - // Raw contains the "result" part of the message, unparsed. - Result RawMessage `json:"result"` -} - -// GetResult parses a result out of `msg` (which must have type -// "result"). If `msg` indicates that an error occurred, convert that -// to an error and return it. Parse the result into `result`, which -// must be unmarshalable as JSON. -func (msg Message) GetResult(result any) error { - if msg.Type != "result" { - return fmt.Errorf( - "response message was not of type 'result': %#v", msg, - ) - } - var resultMsg ResultMessage - if err := json.Unmarshal(msg.Raw, &resultMsg); err != nil { - return fmt.Errorf("unmarshaling result message: %w", err) - } - if !resultMsg.Success { - if resultMsg.Error == nil { - return fmt.Errorf( - "request did not succeed but no error was returned", - ) - } - return resultMsg.Error - } - - if err := json.Unmarshal(resultMsg.Result, result); err != nil { - return fmt.Errorf("unmarshalling result from %q: %w", resultMsg.Result, err) - } - return nil -} - // "state_changed" events are compressed in a rather awkward way. // These types help pick them apart. diff --git a/websocket/result_message.go b/websocket/result_message.go new file mode 100644 index 0000000..6219b9c --- /dev/null +++ b/websocket/result_message.go @@ -0,0 +1,67 @@ +package websocket + +import ( + "encoding/json" + "fmt" +) + +type BaseResultMessage struct { + BaseMessage + Success bool `json:"success"` + Error *ResultError `json:"error,omitempty"` +} + +type ResultError struct { + Code string `json:"code"` + Message string `json:"message"` +} + +func (err *ResultError) Error() string { + switch { + case err.Code != "" && err.Message != "": + return fmt.Sprintf("%s: %s", err.Code, err.Message) + case err.Code == "" && err.Message != "": + return fmt.Sprintf("unknown_error: %s", err.Message) + case err.Code != "" && err.Message == "": + return fmt.Sprintf("%s", err.Code) + default: + // This seems not to be an error at all. + return fmt.Sprintf("INVALID (seems not to be an error)") + } +} + +type ResultMessage struct { + BaseResultMessage + + // Raw contains the "result" part of the message, unparsed. + Result RawMessage `json:"result"` +} + +// GetResult parses a result out of `msg` (which must have type +// "result"). If `msg` indicates that an error occurred, convert that +// to an error and return it. Parse the result into `result`, which +// must be unmarshalable as JSON. +func (msg Message) GetResult(result any) error { + if msg.Type != "result" { + return fmt.Errorf( + "response message was not of type 'result': %#v", msg, + ) + } + var resultMsg ResultMessage + if err := json.Unmarshal(msg.Raw, &resultMsg); err != nil { + return fmt.Errorf("unmarshaling result message: %w", err) + } + if !resultMsg.Success { + if resultMsg.Error == nil { + return fmt.Errorf( + "request did not succeed but no error was returned", + ) + } + return resultMsg.Error + } + + if err := json.Unmarshal(resultMsg.Result, result); err != nil { + return fmt.Errorf("unmarshalling result from %q: %w", resultMsg.Result, err) + } + return nil +} From 17f3dac492ba6539703ce980a1611f9589678006 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 26 May 2024 15:36:57 +0200 Subject: [PATCH 089/103] Move `CompressedStateChangedMessage` definition to a separate file --- websocket/message.go | 149 ---------------------------- websocket/state_changed_message.go | 150 +++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 149 deletions(-) create mode 100644 websocket/state_changed_message.go diff --git a/websocket/message.go b/websocket/message.go index 9dfa681..bd05767 100644 --- a/websocket/message.go +++ b/websocket/message.go @@ -1,10 +1,5 @@ package websocket -import ( - "encoding/json" - "log/slog" -) - // BaseMessage implements the required part of any websocket message. // The idea is to embed this type in other message types. type BaseMessage struct { @@ -34,147 +29,3 @@ type Message struct { // fields `Type` and `ID`, which also appear in `BaseMessage`). Raw RawMessage `json:"-"` } - -// "state_changed" events are compressed in a rather awkward way. -// These types help pick them apart. - -type EntityState struct { - State RawMessage `json:"state"` - Attributes map[string]RawMessage `json:"attributes"` - Context RawMessage `json:"context"` - LastChanged RawMessage `json:"last_changed"` -} - -type EntityStateItem struct { - EntityID string `json:"entity_id"` - EntityState -} - -// CompressedEntityState is similar to `EntityState` except that it -// doesn't include the entity ID and the JSON field names are -// abbreviated. -type CompressedEntityState struct { - State RawMessage `json:"s"` - Attributes map[string]RawMessage `json:"a"` - Context RawMessage `json:"c"` - LastChanged RawMessage `json:"lc"` -} - -// CompressedEntityChange keeps tracks of fields added and removed as -// part of a change. Fields that are mutated appear as "additions". -type CompressedEntityChange struct { - Additions CompressedEntityState `json:"+,omitempty"` - Removals struct { - Attributes []string `json:"a"` - Context []string `json:"c"` - } `json:"-,omitempty"` -} - -type CompressedStateChangedMessage struct { - BaseMessage - Event struct { - Added map[string]CompressedEntityState `json:"a,omitempty"` - Changed map[string]CompressedEntityChange `json:"c,omitempty"` - Removed []string `json:"r,omitempty"` - } `json:"event"` -} - -// Apply applies the changes indicated in `msg` to the entity with the -// specified `entityID` whose old state was `oldState`, returning the -// new state. -func (msg CompressedStateChangedMessage) Apply( - entityID string, oldState EntityState, -) (EntityState, error) { - if state, ok := msg.Event.Added[entityID]; ok { - // This entityID was added. The new state was right there in - // the message. - return EntityState(state), nil - } - if change, ok := msg.Event.Changed[entityID]; ok { - state := oldState.State - if len(change.Additions.State) != 0 { - state = change.Additions.State - } - // The existing entry has had some fields changed. - return EntityState{ - State: state, - Attributes: mergeMaps( - oldState.Attributes, - change.Additions.Attributes, - change.Removals.Attributes, - ), - // FIXME: apparently, context can also be a single string. - Context: mergeContexts( - oldState.Context, - change.Additions.Context, - change.Removals.Context, - ), - LastChanged: change.Additions.LastChanged, - }, nil - } - for _, eid := range msg.Event.Removed { - if eid == entityID { - return EntityState{}, nil - } - } - return oldState, nil -} - -func mergeMaps(old, additions map[string]RawMessage, removals []string) map[string]RawMessage { - new := make(map[string]RawMessage, len(old)+len(additions)-len(removals)) - for k, v := range old { - new[k] = v - } - for k, v := range additions { - new[k] = v - } - for _, k := range removals { - delete(new, k) - } - return new -} - -func mergeContexts(old, additions RawMessage, removals []string) RawMessage { - switch { - case len(old) == 0: - return additions - case old[0] == '"': - // The context is a single string. - if len(additions) != 0 { - return additions - } - return old - case old[0] == '{': - // The context is an object. - var contextMap map[string]RawMessage - if err := json.Unmarshal(old, &contextMap); err != nil { - slog.Error("cannot unmarshal old context", - "old", string(old), - "error", err, - ) - return additions - } - var addMap map[string]RawMessage - if len(additions) != 0 { - if err := json.Unmarshal(additions, &addMap); err != nil { - slog.Error("cannot unmarshal additions", - "additions", string(additions), - "error", err, - ) - return old - } - } - newMap := mergeMaps(contextMap, addMap, removals) - newContext, err := json.Marshal(newMap) - if err != nil { - slog.Error("cannot marshal new context", - "context", newMap, - "error", err, - ) - return old - } - return newContext - default: - return old - } -} diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go new file mode 100644 index 0000000..018f017 --- /dev/null +++ b/websocket/state_changed_message.go @@ -0,0 +1,150 @@ +package websocket + +import ( + "encoding/json" + "log/slog" +) + +// "state_changed" events are compressed in a rather awkward way. +// These types help pick them apart. + +type EntityState struct { + State RawMessage `json:"state"` + Attributes map[string]RawMessage `json:"attributes"` + Context RawMessage `json:"context"` + LastChanged RawMessage `json:"last_changed"` +} + +type EntityStateItem struct { + EntityID string `json:"entity_id"` + EntityState +} + +// CompressedEntityState is similar to `EntityState` except that it +// doesn't include the entity ID and the JSON field names are +// abbreviated. +type CompressedEntityState struct { + State RawMessage `json:"s"` + Attributes map[string]RawMessage `json:"a"` + Context RawMessage `json:"c"` + LastChanged RawMessage `json:"lc"` +} + +// CompressedEntityChange keeps tracks of fields added and removed as +// part of a change. Fields that are mutated appear as "additions". +type CompressedEntityChange struct { + Additions CompressedEntityState `json:"+,omitempty"` + Removals struct { + Attributes []string `json:"a"` + Context []string `json:"c"` + } `json:"-,omitempty"` +} + +type CompressedStateChangedMessage struct { + BaseMessage + Event struct { + Added map[string]CompressedEntityState `json:"a,omitempty"` + Changed map[string]CompressedEntityChange `json:"c,omitempty"` + Removed []string `json:"r,omitempty"` + } `json:"event"` +} + +// Apply applies the changes indicated in `msg` to the entity with the +// specified `entityID` whose old state was `oldState`, returning the +// new state. +func (msg CompressedStateChangedMessage) Apply( + entityID string, oldState EntityState, +) (EntityState, error) { + if state, ok := msg.Event.Added[entityID]; ok { + // This entityID was added. The new state was right there in + // the message. + return EntityState(state), nil + } + if change, ok := msg.Event.Changed[entityID]; ok { + state := oldState.State + if len(change.Additions.State) != 0 { + state = change.Additions.State + } + // The existing entry has had some fields changed. + return EntityState{ + State: state, + Attributes: mergeMaps( + oldState.Attributes, + change.Additions.Attributes, + change.Removals.Attributes, + ), + // FIXME: apparently, context can also be a single string. + Context: mergeContexts( + oldState.Context, + change.Additions.Context, + change.Removals.Context, + ), + LastChanged: change.Additions.LastChanged, + }, nil + } + for _, eid := range msg.Event.Removed { + if eid == entityID { + return EntityState{}, nil + } + } + return oldState, nil +} + +func mergeMaps(old, additions map[string]RawMessage, removals []string) map[string]RawMessage { + new := make(map[string]RawMessage, len(old)+len(additions)-len(removals)) + for k, v := range old { + new[k] = v + } + for k, v := range additions { + new[k] = v + } + for _, k := range removals { + delete(new, k) + } + return new +} + +func mergeContexts(old, additions RawMessage, removals []string) RawMessage { + switch { + case len(old) == 0: + return additions + case old[0] == '"': + // The context is a single string. + if len(additions) != 0 { + return additions + } + return old + case old[0] == '{': + // The context is an object. + var contextMap map[string]RawMessage + if err := json.Unmarshal(old, &contextMap); err != nil { + slog.Error("cannot unmarshal old context", + "old", string(old), + "error", err, + ) + return additions + } + var addMap map[string]RawMessage + if len(additions) != 0 { + if err := json.Unmarshal(additions, &addMap); err != nil { + slog.Error("cannot unmarshal additions", + "additions", string(additions), + "error", err, + ) + return old + } + } + newMap := mergeMaps(contextMap, addMap, removals) + newContext, err := json.Marshal(newMap) + if err != nil { + slog.Error("cannot marshal new context", + "context", newMap, + "error", err, + ) + return old + } + return newContext + default: + return old + } +} From 6d1b0a0b2090ccad4a9581590d32ca98384a9686 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 26 May 2024 16:02:11 +0200 Subject: [PATCH 090/103] EventContext, BaseEvent, Event, EventMessage: new types --- websocket/event_message.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 websocket/event_message.go diff --git a/websocket/event_message.go b/websocket/event_message.go new file mode 100644 index 0000000..3dfced7 --- /dev/null +++ b/websocket/event_message.go @@ -0,0 +1,24 @@ +package websocket + +type EventContext struct { + ID string `json:"id"` + UserID *string `json:"user_id"` + ParentID *string `json:"parent_id"` +} + +type BaseEvent struct { + EventType string `json:"event_type"` + Origin string `json:"origin"` + TimeFired string `json:"time_fired"` + Context EventContext `json:"context"` +} + +type Event struct { + BaseEvent + RawData RawMessage `json:"data"` +} + +type EventMessage struct { + BaseMessage + Event Event `json:"event"` +} From 3ae416003df8c551aa79db57bd364c53aca49153 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 26 May 2024 16:17:32 +0200 Subject: [PATCH 091/103] JWaveJSEventData: new type Extract type `JWaveJSEventData` from `EventZWaveJSValueNotification`. --- app/eventTypes.go | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/app/eventTypes.go b/app/eventTypes.go index 4dfbefb..74396a7 100644 --- a/app/eventTypes.go +++ b/app/eventTypes.go @@ -2,28 +2,30 @@ package app import "time" +type ZWaveJSEventData struct { + Domain string `json:"domain"` + NodeID int `json:"node_id"` + HomeID int64 `json:"home_id"` + Endpoint int `json:"endpoint"` + DeviceID string `json:"device_id"` + CommandClass int `json:"command_class"` + CommandClassName string `json:"command_class_name"` + Label string `json:"label"` + Property string `json:"property"` + PropertyName string `json:"property_name"` + PropertyKey string `json:"property_key"` + PropertyKeyName string `json:"property_key_name"` + Value string `json:"value"` + ValueRaw int `json:"value_raw"` +} + type EventZWaveJSValueNotification struct { ID int `json:"id"` Type string `json:"type"` Event struct { - EventType string `json:"event_type"` - Data struct { - Domain string `json:"domain"` - NodeID int `json:"node_id"` - HomeID int64 `json:"home_id"` - Endpoint int `json:"endpoint"` - DeviceID string `json:"device_id"` - CommandClass int `json:"command_class"` - CommandClassName string `json:"command_class_name"` - Label string `json:"label"` - Property string `json:"property"` - PropertyName string `json:"property_name"` - PropertyKey string `json:"property_key"` - PropertyKeyName string `json:"property_key_name"` - Value string `json:"value"` - ValueRaw int `json:"value_raw"` - } `json:"data"` - Origin string `json:"origin"` - TimeFired time.Time `json:"time_fired"` + EventType string `json:"event_type"` + Data ZWaveJSEventData `json:"data"` + Origin string `json:"origin"` + TimeFired time.Time `json:"time_fired"` } `json:"event"` } From 0d35e28d18288e039c81fb361bc11229f064396c Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 26 May 2024 16:22:16 +0200 Subject: [PATCH 092/103] Change event listeners to take `websocket.Event` as argument --- app/eventListener.go | 20 +++++--------------- app/eventTypes.go | 13 ------------- example/example.go | 18 +++++++++--------- 3 files changed, 14 insertions(+), 37 deletions(-) diff --git a/app/eventListener.go b/app/eventListener.go index dc9e143..37c3656 100644 --- a/app/eventListener.go +++ b/app/eventListener.go @@ -26,7 +26,7 @@ type EventListener struct { disabledEntities []internal.EnabledDisabledInfo } -type EventListenerCallback func(EventData) +type EventListenerCallback func(websocket.Event) type EventData struct { Type string @@ -152,17 +152,11 @@ func (b eventListenerBuilder3) Build() EventListener { return b.eventListener } -type BaseEventMsg struct { - Event struct { - EventType string `json:"event_type"` - } `json:"event"` -} - /* Functions */ func (app *App) callEventListeners(msg websocket.Message) { - baseEventMsg := BaseEventMsg{} - json.Unmarshal(msg.Raw, &baseEventMsg) - listeners, ok := app.eventListeners[baseEventMsg.Event.EventType] + var eventMessage websocket.EventMessage + json.Unmarshal(msg.Raw, &eventMessage) + listeners, ok := app.eventListeners[eventMessage.Event.EventType] if !ok { // no listeners registered for this event type return @@ -189,11 +183,7 @@ func (app *App) callEventListeners(msg websocket.Message) { continue } - eventData := EventData{ - Type: baseEventMsg.Event.EventType, - RawEventJSON: msg.Raw, - } - go l.callback(eventData) + go l.callback(eventMessage.Event) l.lastRan = carbon.Now() } } diff --git a/app/eventTypes.go b/app/eventTypes.go index 74396a7..517a504 100644 --- a/app/eventTypes.go +++ b/app/eventTypes.go @@ -1,7 +1,5 @@ package app -import "time" - type ZWaveJSEventData struct { Domain string `json:"domain"` NodeID int `json:"node_id"` @@ -18,14 +16,3 @@ type ZWaveJSEventData struct { Value string `json:"value"` ValueRaw int `json:"value_raw"` } - -type EventZWaveJSValueNotification struct { - ID int `json:"id"` - Type string `json:"type"` - Event struct { - EventType string `json:"event_type"` - Data ZWaveJSEventData `json:"data"` - Origin string `json:"origin"` - TimeFired time.Time `json:"time_fired"` - } `json:"event"` -} diff --git a/example/example.go b/example/example.go index 8a4a8a0..1e64512 100644 --- a/example/example.go +++ b/example/example.go @@ -10,6 +10,7 @@ import ( ga "saml.dev/gome-assistant" "saml.dev/gome-assistant/app" gaapp "saml.dev/gome-assistant/app" + "saml.dev/gome-assistant/websocket" ) func main() { @@ -75,15 +76,14 @@ func pantryLights(app *app.App, sensor gaapp.EntityData) { } } -func onEvent(data gaapp.EventData) { - // Since the structure of the event changes depending - // on the event type, you can Unmarshal the raw json - // into a Go type. If a type for your event doesn't - // exist, you can write it yourself! PR's welcome to - // the eventTypes.go file :) - ev := gaapp.EventZWaveJSValueNotification{} - json.Unmarshal(data.RawEventJSON, &ev) - slog.Info("On event invoked", "event", ev) +func onEvent(ev websocket.Event) { + // Since the structure of the event data changes depending on the + // event type, you can Unmarshal the data into a Go type. If a + // type for your event doesn't exist, you can write it yourself! + // PR's welcome to the eventTypes.go file :) + var data gaapp.ZWaveJSEventData + json.Unmarshal(ev.RawData, &data) + slog.Info("On event invoked", "data", data) } func lightsOut(app *app.App) { From 1712847dab1d2165b5b88e2ee9655d1aab36ad88 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 09:35:32 +0200 Subject: [PATCH 093/103] `encoding/json` should be able to unmarshal the time string here --- websocket/state_changed_message.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go index 018f017..62001cd 100644 --- a/websocket/state_changed_message.go +++ b/websocket/state_changed_message.go @@ -3,6 +3,7 @@ package websocket import ( "encoding/json" "log/slog" + "time" ) // "state_changed" events are compressed in a rather awkward way. @@ -12,7 +13,7 @@ type EntityState struct { State RawMessage `json:"state"` Attributes map[string]RawMessage `json:"attributes"` Context RawMessage `json:"context"` - LastChanged RawMessage `json:"last_changed"` + LastChanged time.Time `json:"last_changed"` } type EntityStateItem struct { @@ -27,7 +28,7 @@ type CompressedEntityState struct { State RawMessage `json:"s"` Attributes map[string]RawMessage `json:"a"` Context RawMessage `json:"c"` - LastChanged RawMessage `json:"lc"` + LastChanged time.Time `json:"lc"` } // CompressedEntityChange keeps tracks of fields added and removed as From cd413cc9d9fdc5c64cd6408dc47e24cfeb385c3d Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 09:36:56 +0200 Subject: [PATCH 094/103] Rename `EntityState` to `Entity` --- websocket/state_changed_message.go | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go index 62001cd..0a75236 100644 --- a/websocket/state_changed_message.go +++ b/websocket/state_changed_message.go @@ -9,22 +9,21 @@ import ( // "state_changed" events are compressed in a rather awkward way. // These types help pick them apart. -type EntityState struct { +type Entity struct { State RawMessage `json:"state"` Attributes map[string]RawMessage `json:"attributes"` Context RawMessage `json:"context"` LastChanged time.Time `json:"last_changed"` } -type EntityStateItem struct { +type EntityItem struct { EntityID string `json:"entity_id"` - EntityState + Entity } -// CompressedEntityState is similar to `EntityState` except that it -// doesn't include the entity ID and the JSON field names are -// abbreviated. -type CompressedEntityState struct { +// CompressedEntity is similar to `Entity` except that the JSON field +// names are abbreviated. +type CompressedEntity struct { State RawMessage `json:"s"` Attributes map[string]RawMessage `json:"a"` Context RawMessage `json:"c"` @@ -34,7 +33,7 @@ type CompressedEntityState struct { // CompressedEntityChange keeps tracks of fields added and removed as // part of a change. Fields that are mutated appear as "additions". type CompressedEntityChange struct { - Additions CompressedEntityState `json:"+,omitempty"` + Additions CompressedEntity `json:"+,omitempty"` Removals struct { Attributes []string `json:"a"` Context []string `json:"c"` @@ -44,7 +43,7 @@ type CompressedEntityChange struct { type CompressedStateChangedMessage struct { BaseMessage Event struct { - Added map[string]CompressedEntityState `json:"a,omitempty"` + Added map[string]CompressedEntity `json:"a,omitempty"` Changed map[string]CompressedEntityChange `json:"c,omitempty"` Removed []string `json:"r,omitempty"` } `json:"event"` @@ -54,12 +53,12 @@ type CompressedStateChangedMessage struct { // specified `entityID` whose old state was `oldState`, returning the // new state. func (msg CompressedStateChangedMessage) Apply( - entityID string, oldState EntityState, -) (EntityState, error) { + entityID string, oldState Entity, +) (Entity, error) { if state, ok := msg.Event.Added[entityID]; ok { // This entityID was added. The new state was right there in // the message. - return EntityState(state), nil + return Entity(state), nil } if change, ok := msg.Event.Changed[entityID]; ok { state := oldState.State @@ -67,7 +66,7 @@ func (msg CompressedStateChangedMessage) Apply( state = change.Additions.State } // The existing entry has had some fields changed. - return EntityState{ + return Entity{ State: state, Attributes: mergeMaps( oldState.Attributes, @@ -85,7 +84,7 @@ func (msg CompressedStateChangedMessage) Apply( } for _, eid := range msg.Event.Removed { if eid == entityID { - return EntityState{}, nil + return Entity{}, nil } } return oldState, nil From f278132d454a7575312ee9ead09cd09d69bb7fe8 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 09:51:14 +0200 Subject: [PATCH 095/103] EntityState: new type The entity state should always be a string, so add a string-based helper class to hold it. --- websocket/state_changed_message.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go index 0a75236..e6144c5 100644 --- a/websocket/state_changed_message.go +++ b/websocket/state_changed_message.go @@ -9,8 +9,24 @@ import ( // "state_changed" events are compressed in a rather awkward way. // These types help pick them apart. +// EntityState is the state of an entity ( // E.g., "on", "off", +// "unavailable"; there are probably more. +type EntityState string + +func (s EntityState) On() bool { + return s == "on" +} + +func (s EntityState) Off() bool { + return s == "off" +} + +func (s EntityState) Unavailable() bool { + return s == "unavailable" +} + type Entity struct { - State RawMessage `json:"state"` + State EntityState `json:"state"` Attributes map[string]RawMessage `json:"attributes"` Context RawMessage `json:"context"` LastChanged time.Time `json:"last_changed"` @@ -24,7 +40,7 @@ type EntityItem struct { // CompressedEntity is similar to `Entity` except that the JSON field // names are abbreviated. type CompressedEntity struct { - State RawMessage `json:"s"` + State EntityState `json:"s"` Attributes map[string]RawMessage `json:"a"` Context RawMessage `json:"c"` LastChanged time.Time `json:"lc"` From e818956441a80b8112f3bea5014e8a04257c1a5e Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 10:03:30 +0200 Subject: [PATCH 096/103] According to the Python, the context ID can be null --- websocket/event_message.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket/event_message.go b/websocket/event_message.go index 3dfced7..f65daf6 100644 --- a/websocket/event_message.go +++ b/websocket/event_message.go @@ -1,7 +1,7 @@ package websocket type EventContext struct { - ID string `json:"id"` + ID *string `json:"id"` UserID *string `json:"user_id"` ParentID *string `json:"parent_id"` } From 49d8b8e58b5b79367a0467ef45554c9478c34df7 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 10:06:30 +0200 Subject: [PATCH 097/103] `encoding/json` should be able to unmarshal the time string here --- websocket/event_message.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/websocket/event_message.go b/websocket/event_message.go index f65daf6..ba0b57a 100644 --- a/websocket/event_message.go +++ b/websocket/event_message.go @@ -1,5 +1,7 @@ package websocket +import "time" + type EventContext struct { ID *string `json:"id"` UserID *string `json:"user_id"` @@ -9,7 +11,7 @@ type EventContext struct { type BaseEvent struct { EventType string `json:"event_type"` Origin string `json:"origin"` - TimeFired string `json:"time_fired"` + TimeFired time.Time `json:"time_fired"` Context EventContext `json:"context"` } From e027a8ec47cfd1cb20da16ed5de15d91927c5580 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 10:20:08 +0200 Subject: [PATCH 098/103] entity.go: new file, split out from `state_changed_message.go` --- websocket/entity.go | 45 ++++++++++++++++++++++++++++++ websocket/state_changed_message.go | 38 ------------------------- 2 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 websocket/entity.go diff --git a/websocket/entity.go b/websocket/entity.go new file mode 100644 index 0000000..3c84ca5 --- /dev/null +++ b/websocket/entity.go @@ -0,0 +1,45 @@ +package websocket + +import ( + "time" +) + +// "state_changed" events are compressed in a rather awkward way. +// These types help pick them apart. + +type Entity struct { + State EntityState `json:"state"` + Attributes map[string]RawMessage `json:"attributes"` + Context RawMessage `json:"context"` + LastChanged time.Time `json:"last_changed"` +} + +type EntityItem struct { + EntityID string `json:"entity_id"` + Entity +} + +// CompressedEntity is similar to `Entity` except that the JSON field +// names are abbreviated. +type CompressedEntity struct { + State EntityState `json:"s"` + Attributes map[string]RawMessage `json:"a"` + Context RawMessage `json:"c"` + LastChanged time.Time `json:"lc"` +} + +// EntityState is the state of an entity ( // E.g., "on", "off", +// "unavailable"; there are probably more. +type EntityState string + +func (s EntityState) On() bool { + return s == "on" +} + +func (s EntityState) Off() bool { + return s == "off" +} + +func (s EntityState) Unavailable() bool { + return s == "unavailable" +} diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go index e6144c5..649c06d 100644 --- a/websocket/state_changed_message.go +++ b/websocket/state_changed_message.go @@ -3,49 +3,11 @@ package websocket import ( "encoding/json" "log/slog" - "time" ) // "state_changed" events are compressed in a rather awkward way. // These types help pick them apart. -// EntityState is the state of an entity ( // E.g., "on", "off", -// "unavailable"; there are probably more. -type EntityState string - -func (s EntityState) On() bool { - return s == "on" -} - -func (s EntityState) Off() bool { - return s == "off" -} - -func (s EntityState) Unavailable() bool { - return s == "unavailable" -} - -type Entity struct { - State EntityState `json:"state"` - Attributes map[string]RawMessage `json:"attributes"` - Context RawMessage `json:"context"` - LastChanged time.Time `json:"last_changed"` -} - -type EntityItem struct { - EntityID string `json:"entity_id"` - Entity -} - -// CompressedEntity is similar to `Entity` except that the JSON field -// names are abbreviated. -type CompressedEntity struct { - State EntityState `json:"s"` - Attributes map[string]RawMessage `json:"a"` - Context RawMessage `json:"c"` - LastChanged time.Time `json:"lc"` -} - // CompressedEntityChange keeps tracks of fields added and removed as // part of a change. Fields that are mutated appear as "additions". type CompressedEntityChange struct { From a472d8146bf371d1fec5f9c00ef10658e7a83775 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 10:26:45 +0200 Subject: [PATCH 099/103] RawObject: new type --- websocket/entity.go | 16 ++++++++-------- websocket/raw_message.go | 3 +++ websocket/state_changed_message.go | 8 ++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/websocket/entity.go b/websocket/entity.go index 3c84ca5..9a83194 100644 --- a/websocket/entity.go +++ b/websocket/entity.go @@ -8,10 +8,10 @@ import ( // These types help pick them apart. type Entity struct { - State EntityState `json:"state"` - Attributes map[string]RawMessage `json:"attributes"` - Context RawMessage `json:"context"` - LastChanged time.Time `json:"last_changed"` + State EntityState `json:"state"` + Attributes RawObject `json:"attributes"` + Context RawMessage `json:"context"` + LastChanged time.Time `json:"last_changed"` } type EntityItem struct { @@ -22,10 +22,10 @@ type EntityItem struct { // CompressedEntity is similar to `Entity` except that the JSON field // names are abbreviated. type CompressedEntity struct { - State EntityState `json:"s"` - Attributes map[string]RawMessage `json:"a"` - Context RawMessage `json:"c"` - LastChanged time.Time `json:"lc"` + State EntityState `json:"s"` + Attributes RawObject `json:"a"` + Context RawMessage `json:"c"` + LastChanged time.Time `json:"lc"` } // EntityState is the state of an entity ( // E.g., "on", "off", diff --git a/websocket/raw_message.go b/websocket/raw_message.go index ad4ee22..f28ceec 100644 --- a/websocket/raw_message.go +++ b/websocket/raw_message.go @@ -24,3 +24,6 @@ func (m *RawMessage) UnmarshalJSON(data []byte) error { func (rm RawMessage) String() string { return string(rm) } + +// RawObject is a minimally-parsed representation of a JSON object. +type RawObject map[string]RawMessage diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go index 649c06d..bde2495 100644 --- a/websocket/state_changed_message.go +++ b/websocket/state_changed_message.go @@ -68,8 +68,8 @@ func (msg CompressedStateChangedMessage) Apply( return oldState, nil } -func mergeMaps(old, additions map[string]RawMessage, removals []string) map[string]RawMessage { - new := make(map[string]RawMessage, len(old)+len(additions)-len(removals)) +func mergeMaps(old, additions RawObject, removals []string) RawObject { + new := make(RawObject, len(old)+len(additions)-len(removals)) for k, v := range old { new[k] = v } @@ -94,7 +94,7 @@ func mergeContexts(old, additions RawMessage, removals []string) RawMessage { return old case old[0] == '{': // The context is an object. - var contextMap map[string]RawMessage + var contextMap RawObject if err := json.Unmarshal(old, &contextMap); err != nil { slog.Error("cannot unmarshal old context", "old", string(old), @@ -102,7 +102,7 @@ func mergeContexts(old, additions RawMessage, removals []string) RawMessage { ) return additions } - var addMap map[string]RawMessage + var addMap RawObject if len(additions) != 0 { if err := json.Unmarshal(additions, &addMap); err != nil { slog.Error("cannot unmarshal additions", From 17c832a78ebe8e0e059996429a73dd115b883a64 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 10:30:10 +0200 Subject: [PATCH 100/103] Partly generify the `Entity` types --- websocket/entity.go | 12 ++++++------ websocket/state_changed_message.go | 21 +++++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/websocket/entity.go b/websocket/entity.go index 9a83194..411aed8 100644 --- a/websocket/entity.go +++ b/websocket/entity.go @@ -7,23 +7,23 @@ import ( // "state_changed" events are compressed in a rather awkward way. // These types help pick them apart. -type Entity struct { +type Entity[AttributesT any] struct { State EntityState `json:"state"` - Attributes RawObject `json:"attributes"` + Attributes AttributesT `json:"attributes"` Context RawMessage `json:"context"` LastChanged time.Time `json:"last_changed"` } -type EntityItem struct { +type EntityItem[AttributesT any] struct { EntityID string `json:"entity_id"` - Entity + Entity[AttributesT] } // CompressedEntity is similar to `Entity` except that the JSON field // names are abbreviated. -type CompressedEntity struct { +type CompressedEntity[AttributesT any] struct { State EntityState `json:"s"` - Attributes RawObject `json:"a"` + Attributes AttributesT `json:"a"` Context RawMessage `json:"c"` LastChanged time.Time `json:"lc"` } diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go index bde2495..f74fe05 100644 --- a/websocket/state_changed_message.go +++ b/websocket/state_changed_message.go @@ -11,7 +11,7 @@ import ( // CompressedEntityChange keeps tracks of fields added and removed as // part of a change. Fields that are mutated appear as "additions". type CompressedEntityChange struct { - Additions CompressedEntity `json:"+,omitempty"` + Additions CompressedEntity[RawObject] `json:"+,omitempty"` Removals struct { Attributes []string `json:"a"` Context []string `json:"c"` @@ -21,22 +21,23 @@ type CompressedEntityChange struct { type CompressedStateChangedMessage struct { BaseMessage Event struct { - Added map[string]CompressedEntity `json:"a,omitempty"` - Changed map[string]CompressedEntityChange `json:"c,omitempty"` - Removed []string `json:"r,omitempty"` + Added map[string]CompressedEntity[RawObject] `json:"a,omitempty"` + Changed map[string]CompressedEntityChange `json:"c,omitempty"` + Removed []string `json:"r,omitempty"` } `json:"event"` } // Apply applies the changes indicated in `msg` to the entity with the // specified `entityID` whose old state was `oldState`, returning the -// new state. +// new state. If the entity was removed altogether, the return value +// is an empty entity. func (msg CompressedStateChangedMessage) Apply( - entityID string, oldState Entity, -) (Entity, error) { + entityID string, oldState Entity[RawObject], +) (Entity[RawObject], error) { if state, ok := msg.Event.Added[entityID]; ok { // This entityID was added. The new state was right there in // the message. - return Entity(state), nil + return Entity[RawObject](state), nil } if change, ok := msg.Event.Changed[entityID]; ok { state := oldState.State @@ -44,7 +45,7 @@ func (msg CompressedStateChangedMessage) Apply( state = change.Additions.State } // The existing entry has had some fields changed. - return Entity{ + return Entity[RawObject]{ State: state, Attributes: mergeMaps( oldState.Attributes, @@ -62,7 +63,7 @@ func (msg CompressedStateChangedMessage) Apply( } for _, eid := range msg.Event.Removed { if eid == entityID { - return Entity{}, nil + return Entity[RawObject]{}, nil } } return oldState, nil From 200eb25e73d8e94b25b7f11eaf09ca25432324bb Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 13:09:11 +0200 Subject: [PATCH 101/103] ApplyChange(): function to apply changes to an arbitrary entity Change `CompressedStateChangedMessage.Apply()` from a method into a generic top-level function, which can take an arbitrary `Entity` type as argument and apply the change to it. This involves some type conversion, which is done by serializing to and from JSON a couple of times, but at least the application code doesn't need to be written separately for each type. --- websocket/state_changed_message.go | 125 +++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go index f74fe05..49a09c5 100644 --- a/websocket/state_changed_message.go +++ b/websocket/state_changed_message.go @@ -2,6 +2,7 @@ package websocket import ( "encoding/json" + "fmt" "log/slog" ) @@ -27,46 +28,86 @@ type CompressedStateChangedMessage struct { } `json:"event"` } -// Apply applies the changes indicated in `msg` to the entity with the -// specified `entityID` whose old state was `oldState`, returning the -// new state. If the entity was removed altogether, the return value -// is an empty entity. -func (msg CompressedStateChangedMessage) Apply( - entityID string, oldState Entity[RawObject], -) (Entity[RawObject], error) { - if state, ok := msg.Event.Added[entityID]; ok { +// ApplyChange applies the changes indicated in `msg` to the entity with the +// specified `entityID` and whose old state was `oldEntity`, returning the +// new entity. If the entity was removed altogether, return an empty +// entity. +// +// Because the entity being changed might not store its attributes as +// a generic `RawObject`, we have to do the conversion in an awkward +// way to avoiding needing specialized code for each `AttributeT`: + +// 1. Convert the old attributes from an `AttributeT` into a +// `RawObject`; +// 2. Apply the attribute changes to the `RawObject`; +// 3. Convert the updated `RawObject` back into an `AttributeT`. +func ApplyChange[AttributeT any]( + msg CompressedStateChangedMessage, + entityID string, oldEntity Entity[AttributeT], +) (Entity[AttributeT], error) { + for _, eid := range msg.Event.Removed { + if eid == entityID { + return Entity[AttributeT]{}, nil + } + } + + if entity, ok := msg.Event.Added[entityID]; ok { // This entityID was added. The new state was right there in // the message. - return Entity[RawObject](state), nil - } - if change, ok := msg.Event.Changed[entityID]; ok { - state := oldState.State - if len(change.Additions.State) != 0 { - state = change.Additions.State + var newAttributes AttributeT + if err := convertTypes(&newAttributes, entity.Attributes); err != nil { + return Entity[AttributeT]{}, fmt.Errorf( + "converting the added attributes: %w", err, + ) } - // The existing entry has had some fields changed. - return Entity[RawObject]{ - State: state, - Attributes: mergeMaps( - oldState.Attributes, - change.Additions.Attributes, - change.Removals.Attributes, - ), + return Entity[AttributeT]{ + State: entity.State, + Attributes: newAttributes, // FIXME: apparently, context can also be a single string. - Context: mergeContexts( - oldState.Context, - change.Additions.Context, - change.Removals.Context, - ), - LastChanged: change.Additions.LastChanged, + Context: entity.Context, + LastChanged: entity.LastChanged, }, nil } - for _, eid := range msg.Event.Removed { - if eid == entityID { - return Entity[RawObject]{}, nil - } + + change, ok := msg.Event.Changed[entityID] + if !ok { + // There were no changes. + return oldEntity, nil + } + + // The existing entry has had some fields changed. Apply them to + // `entity` to produce the new entity: + + newEntity := Entity[AttributeT]{ + State: oldEntity.State, + Context: mergeContexts( + oldEntity.Context, + change.Additions.Context, + change.Removals.Context, + ), + LastChanged: change.Additions.LastChanged, } - return oldState, nil + + if change.Additions.State != "" { + newEntity.State = change.Additions.State + } + + var oldAttributes RawObject + if err := convertTypes(&oldAttributes, oldEntity.Attributes); err != nil { + return Entity[AttributeT]{}, fmt.Errorf("converting the old attributes: %w", err) + } + + attributes := mergeMaps( + oldAttributes, + change.Additions.Attributes, + change.Removals.Attributes, + ) + + if err := convertTypes(&newEntity.Attributes, attributes); err != nil { + return Entity[AttributeT]{}, fmt.Errorf("converting the new attributes: %w", err) + } + + return newEntity, nil } func mergeMaps(old, additions RawObject, removals []string) RawObject { @@ -127,3 +168,21 @@ func mergeContexts(old, additions RawMessage, removals []string) RawMessage { return old } } + +// Convert `src` to `dst` (which can be of two different types) by +// serializing to JSON then deserializing. `src` must be something +// that can be passed to `json.Marshal()`, and `dst` must be something +// that can be passed to `json.Unmarshal()` (i.e., typically a +// pointer). +func convertTypes(dst any, src any) error { + b, err := json.Marshal(src) + if err != nil { + return fmt.Errorf("serializing src: %w", err) + } + + if err := json.Unmarshal(b, dst); err != nil { + return fmt.Errorf("deserializing to dst: %w", err) + } + + return nil +} From 87dd38d320d840a89732423a7228fdc48414e693 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sun, 2 Jun 2024 22:50:42 +0200 Subject: [PATCH 102/103] Use a specialized `Context` type in more places --- websocket/context.go | 39 ++++++++++++++++++ websocket/entity.go | 4 +- websocket/event_message.go | 14 ++----- websocket/state_changed_message.go | 65 +++++++++++------------------- 4 files changed, 68 insertions(+), 54 deletions(-) create mode 100644 websocket/context.go diff --git a/websocket/context.go b/websocket/context.go new file mode 100644 index 0000000..d0a9dea --- /dev/null +++ b/websocket/context.go @@ -0,0 +1,39 @@ +package websocket + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" +) + +type Context struct { + ID *string `json:"id"` + UserID *string `json:"user_id"` + ParentID *string `json:"parent_id"` +} + +func (c *Context) UnmarshalJSON(b []byte) error { + if bytes.Equal(b, []byte("null")) { + return nil + } + if b[0] == '"' { + // The context is stored as a naked string. I think this can + // happen but I don't know what it's supposed to signify. + slog.Info("bare string as context; ignored", "input", string(b)) + return nil + } + + // Unmarshal into a type that is assignable to Context but without + // an `UnmarshalJSON()` method: + var context struct { + ID *string `json:"id"` + UserID *string `json:"user_id"` + ParentID *string `json:"parent_id"` + } + if err := json.Unmarshal(b, &context); err != nil { + return fmt.Errorf("unmarshaling context '%s': %w", string(b), err) + } + *c = context + return nil +} diff --git a/websocket/entity.go b/websocket/entity.go index 411aed8..92231bc 100644 --- a/websocket/entity.go +++ b/websocket/entity.go @@ -10,7 +10,7 @@ import ( type Entity[AttributesT any] struct { State EntityState `json:"state"` Attributes AttributesT `json:"attributes"` - Context RawMessage `json:"context"` + Context Context `json:"context"` LastChanged time.Time `json:"last_changed"` } @@ -24,7 +24,7 @@ type EntityItem[AttributesT any] struct { type CompressedEntity[AttributesT any] struct { State EntityState `json:"s"` Attributes AttributesT `json:"a"` - Context RawMessage `json:"c"` + Context Context `json:"c"` LastChanged time.Time `json:"lc"` } diff --git a/websocket/event_message.go b/websocket/event_message.go index ba0b57a..e913ea4 100644 --- a/websocket/event_message.go +++ b/websocket/event_message.go @@ -2,17 +2,11 @@ package websocket import "time" -type EventContext struct { - ID *string `json:"id"` - UserID *string `json:"user_id"` - ParentID *string `json:"parent_id"` -} - type BaseEvent struct { - EventType string `json:"event_type"` - Origin string `json:"origin"` - TimeFired time.Time `json:"time_fired"` - Context EventContext `json:"context"` + EventType string `json:"event_type"` + Origin string `json:"origin"` + TimeFired time.Time `json:"time_fired"` + Context Context `json:"context"` } type Event struct { diff --git a/websocket/state_changed_message.go b/websocket/state_changed_message.go index 49a09c5..f6e9e0e 100644 --- a/websocket/state_changed_message.go +++ b/websocket/state_changed_message.go @@ -3,7 +3,6 @@ package websocket import ( "encoding/json" "fmt" - "log/slog" ) // "state_changed" events are compressed in a rather awkward way. @@ -124,49 +123,31 @@ func mergeMaps(old, additions RawObject, removals []string) RawObject { return new } -func mergeContexts(old, additions RawMessage, removals []string) RawMessage { - switch { - case len(old) == 0: - return additions - case old[0] == '"': - // The context is a single string. - if len(additions) != 0 { - return additions - } - return old - case old[0] == '{': - // The context is an object. - var contextMap RawObject - if err := json.Unmarshal(old, &contextMap); err != nil { - slog.Error("cannot unmarshal old context", - "old", string(old), - "error", err, - ) - return additions - } - var addMap RawObject - if len(additions) != 0 { - if err := json.Unmarshal(additions, &addMap); err != nil { - slog.Error("cannot unmarshal additions", - "additions", string(additions), - "error", err, - ) - return old - } - } - newMap := mergeMaps(contextMap, addMap, removals) - newContext, err := json.Marshal(newMap) - if err != nil { - slog.Error("cannot marshal new context", - "context", newMap, - "error", err, - ) - return old +func mergeContexts(context, additions Context, removals []string) Context { + // Adjust context for any additions: + if additions.ID != nil { + context.ID = additions.ID + } + if additions.UserID != nil { + context.UserID = additions.UserID + } + if additions.ParentID != nil { + context.ParentID = additions.ParentID + } + + // Adjust context for any removals: + for _, key := range removals { + switch key { + case "user_id": + context.UserID = nil + case "id": + context.ID = nil + case "parent_id": + context.ParentID = nil } - return newContext - default: - return old } + + return context } // Convert `src` to `dst` (which can be of two different types) by From 22584dd2d0ff03d486b7f0147222fd6263637654 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Sat, 15 Jun 2024 21:54:05 +0200 Subject: [PATCH 103/103] Timestamps can be either strings or floats --- app/entitylistener.go | 10 +++++----- app/state.go | 9 ++++----- websocket/entity.go | 8 ++------ websocket/time_stamp.go | 29 +++++++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 16 deletions(-) create mode 100644 websocket/time_stamp.go diff --git a/app/entitylistener.go b/app/entitylistener.go index a2621e6..b6625e9 100644 --- a/app/entitylistener.go +++ b/app/entitylistener.go @@ -42,7 +42,7 @@ type EntityData struct { FromAttributes map[string]any ToState string ToAttributes map[string]any - LastChanged time.Time + LastChanged websocket.TimeStamp } type stateChangedMsg struct { @@ -59,10 +59,10 @@ type stateChangedMsg struct { } type msgState struct { - EntityID string `json:"entity_id"` - LastChanged time.Time `json:"last_changed"` - State string `json:"state"` - Attributes map[string]any `json:"attributes"` + EntityID string `json:"entity_id"` + LastChanged websocket.TimeStamp `json:"last_changed"` + State string `json:"state"` + Attributes map[string]any `json:"attributes"` } /* Methods */ diff --git a/app/state.go b/app/state.go index 977e45a..a834475 100644 --- a/app/state.go +++ b/app/state.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "fmt" - "time" "github.com/golang-module/carbon" "saml.dev/gome-assistant/internal/http" @@ -30,10 +29,10 @@ type StateImpl struct { } type EntityState struct { - EntityID string `json:"entity_id"` - State string `json:"state"` - Attributes map[string]any `json:"attributes"` - LastChanged time.Time `json:"last_changed"` + EntityID string `json:"entity_id"` + State string `json:"state"` + Attributes map[string]any `json:"attributes"` + LastChanged websocket.TimeStamp `json:"last_changed"` // The whole message, in JSON format: Raw websocket.RawMessage `json:"-"` diff --git a/websocket/entity.go b/websocket/entity.go index 92231bc..8b9449a 100644 --- a/websocket/entity.go +++ b/websocket/entity.go @@ -1,9 +1,5 @@ package websocket -import ( - "time" -) - // "state_changed" events are compressed in a rather awkward way. // These types help pick them apart. @@ -11,7 +7,7 @@ type Entity[AttributesT any] struct { State EntityState `json:"state"` Attributes AttributesT `json:"attributes"` Context Context `json:"context"` - LastChanged time.Time `json:"last_changed"` + LastChanged TimeStamp `json:"last_changed"` } type EntityItem[AttributesT any] struct { @@ -25,7 +21,7 @@ type CompressedEntity[AttributesT any] struct { State EntityState `json:"s"` Attributes AttributesT `json:"a"` Context Context `json:"c"` - LastChanged time.Time `json:"lc"` + LastChanged TimeStamp `json:"lc"` } // EntityState is the state of an entity ( // E.g., "on", "off", diff --git a/websocket/time_stamp.go b/websocket/time_stamp.go new file mode 100644 index 0000000..e4f67e5 --- /dev/null +++ b/websocket/time_stamp.go @@ -0,0 +1,29 @@ +package websocket + +import ( + "encoding/json" + "fmt" + "math" + "time" +) + +type TimeStamp time.Time + +// UnmarshalJSON unmarshals a timestamp from JSON. HA sometimes +// formats timestamps as RFC 3339 strings, sometimes as fractional +// seconds since the epoch. Handle either one (without recording which +// one it was). +func (ts *TimeStamp) UnmarshalJSON(b []byte) error { + if err := (*time.Time)(ts).UnmarshalJSON(b); err == nil { + return nil + } + + var v float64 + if err := json.Unmarshal(b, &v); err == nil { + seconds := math.Floor(v) + *(*time.Time)(ts) = time.Unix(int64(seconds), int64((v-seconds)*1e+9)) + return nil + } + + return fmt.Errorf("unmarshaling timestamp: '%s'", string(b)) +}