Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/codetests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.11.2
version: v2.11
# Runs golangci-lint on linux against linux and windows.
golangci-linux:
strategy:
Expand All @@ -57,4 +57,4 @@ jobs:
- name: golangci-lint
uses: golangci/golangci-lint-action@v9
with:
version: v2.11.2
version: v2.11
1 change: 1 addition & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ linters:
- funcorder
- noinlineerr
- exhaustive
- nlreturn
settings:
gocritic:
enable-all: true
Expand Down
67 changes: 38 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## OVERVIEW

Full Featured Go Library for [SecuritySpy](https://www.bensoftware.com/securityspy/)'s
(Nearly) Full Featured Go Library for [SecuritySpy](https://www.bensoftware.com/securityspy/)'s
web API. Read about the [API here](https://www.bensoftware.com/securityspy/web-server-spec.html).

Everything is reasonably tested and working. Feedback is welcomed!
Expand All @@ -15,53 +15,60 @@ A command line interface app that uses this library exists. Most of the testing
Find it here: [https://github.com/davidnewhall/SecSpyCLI](https://github.com/davidnewhall/SecSpyCLI)
It's full of great examples on how to use this library, and can be easily installed with homebrew.

- Works with SecuritySpy 4 and 5 and probably 6.
- There's a lot more to learn about this package in [GODOC](https://godoc.org/golift.io/securityspy).
- Works with SecuritySpy 4 and 5 and probably 6.
- There's a lot more to learn about this package in [GODOC](https://godoc.org/golift.io/securityspy).

## BREAKING CHANGES 3/7/2026

- The internal `Server.API` override/testing hook was removed.
- Tests now use `httptest.Server` instead of swapping internal interfaces or generated mocks.
- If you previously replaced `Server.API` in downstream tests, migrate to an HTTP test server and point
`server.Config.URL` at that server.

## FEATURES

#### Server
### Server

- All server and system Info is exposed with one API web request.
- Schedule Presets can be retrieved and invoked.
- All server and system Info is exposed with one API web request.
- Schedule Presets can be retrieved and invoked.

#### Settings
### Settings

- No support for settings yet.
- No support for settings yet.

#### Cameras
### Cameras

- Stream live H264 or MJPEG video from an `io.ReadCloser`.
- Stream live G711 audio from an `io.ReadCloser`.
- Submit G711 audio (files or microphone) to a camera from an `io.ReadCloser`.
- Save live video snippets locally (requires `FFMPEG`).
- Get live JPEG images in `image` format, or save files locally.
- Arm and Disarm actions, motion capture and continuous capture.
- Trigger Motion.
- Set schedules and schedule overrides.
- Inspect PTZ capabilities.
- Control all PTZ actions including invoking and saving presets.
- Stream live H264 or MJPEG video from an `io.ReadCloser`.
- Stream live G711 audio from an `io.ReadCloser`.
- Submit G711 audio (files or microphone) to a camera from an `io.ReadCloser`.
- Save live video snippets locally (requires `FFMPEG`).
- Get live JPEG images in `image` format, or save files locally.
- Arm and Disarm actions, motion capture and continuous capture.
- Trigger Motion.
- Set schedules and schedule overrides.
- Inspect PTZ capabilities.
- Control all PTZ actions including invoking and saving presets.

#### Events
### Events

SecuritySpy has a handy event stream; you can bind functions and/or channels to
all or specific events. When a bound event fires the callback method it's bound
to is run. In the case of a channel binding, the event is sent to the channel
for consumption by a worker (pool).

- Exposes all SecuritySpy events.
- Exposes 6 custom events.
- Method to inject custom events into the event stream.
- Exposes all SecuritySpy events.
- Exposes 6 custom events.
- Method to inject custom events into the event stream.

#### Files
### Files

SecuritySpy saves video and image files based on motion and continuous capture
settings. These files can be listed and downloaded with this library.

- List and retrieve captured images.
- List and retrieve continuous captured videos.
- List and retrieve motion captured videos.
- Save files locally or stream from `io.ReadCloser`.
- List and retrieve captured images.
- List and retrieve continuous captured videos.
- List and retrieve motion captured videos.
- Save files locally or stream from `io.ReadCloser`.

## EXAMPLE

Expand Down Expand Up @@ -113,8 +120,10 @@ func main() {
}
}
```

The output looks like this:
```

```none
SecuritySpy 4.2.10b9 @ 2019-02-09 16:20:00 -0700 MST (http://192.168.1.1:8000/) 7 cameras, 18 scripts, 20 sounds, 6 schedules, 1 schedule presets
0: Porch (2304x1296 ONVIF/Network 192.168.1.12) connected: true, down 0s, modes: C:armed M:armed A:armed, 20FPS, Audio:yes, MD: yes/pre:3s/post:10s idle 3h5m5s Script: SS_SendiMessages.scpt (reset 1m0s)
1: Door (2592x1520 ONVIF/Network 192.168.1.13) connected: true, down 0s, modes: C:armed M:armed A:armed, 15FPS, Audio:yes, MD: yes/pre:4s/post: 5s idle 9m24s Script: SS_SendiMessages.scpt (reset 1m0s)
Expand Down
59 changes: 23 additions & 36 deletions cameras_commands_test.go
Original file line number Diff line number Diff line change
@@ -1,67 +1,54 @@
package securityspy_test

import (
"encoding/xml"
"net/url"
"testing"

"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"golift.io/securityspy"
"golift.io/securityspy/mocks"
"golift.io/securityspy/server"
)

func testServerWithCamera(t *testing.T) (*securityspy.Server, *mocks.MockAPI, *securityspy.Camera) {
t.Helper()

mockCtrl := gomock.NewController(t)
t.Cleanup(mockCtrl.Finish)

secspyServer := securityspy.NewMust(
&server.Config{Username: "user", Password: "pass", URL: "http://127.0.0.1:5678", VerifySSL: false})
fake := mocks.NewMockAPI(mockCtrl)
secspyServer.API = fake

fake.EXPECT().GetXML(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Do(
func(_, _, v any) {
_ = xml.Unmarshal([]byte(testSystemInfo), &v)
},
)
require.NoError(t, secspyServer.Refresh())

camera := secspyServer.Cameras.ByNum(1)
require.NotNil(t, camera)

return secspyServer, fake, camera
}

func TestToggleContinuousUsesNumericArmValues(t *testing.T) {
t.Parallel()

_, fake, camera := testServerWithCamera(t)
_, recorder, camera := testServerWithCamera(t)

fake.EXPECT().SimpleReq("++ssControlContinuous", url.Values{"arm": []string{"0"}}, camera.Number)
require.NoError(t, camera.ToggleContinuous(securityspy.CameraDisarm))

fake.EXPECT().SimpleReq("++ssControlContinuous", url.Values{"arm": []string{"1"}}, camera.Number)
req, found := recorder.findLast("/++ssControlContinuous")
require.True(t, found)
require.Equal(t, "0", req.Query.Get("arm"))
require.Equal(t, "1", req.Query.Get("cameraNum"))

require.NoError(t, camera.ToggleContinuous(securityspy.CameraArm))

req, found = recorder.findLast("/++ssControlContinuous")
require.True(t, found)
require.Equal(t, "1", req.Query.Get("arm"))
require.Equal(t, "1", req.Query.Get("cameraNum"))
}

func TestToggleMotionUsesNumericArmValues(t *testing.T) {
t.Parallel()

_, fake, camera := testServerWithCamera(t)
_, recorder, camera := testServerWithCamera(t)

fake.EXPECT().SimpleReq("++ssControlMotionCapture", url.Values{"arm": []string{"1"}}, camera.Number)
require.NoError(t, camera.ToggleMotion(securityspy.CameraArm))

req, found := recorder.findLast("/++ssControlMotionCapture")
require.True(t, found)
require.Equal(t, "1", req.Query.Get("arm"))
require.Equal(t, "1", req.Query.Get("cameraNum"))
}

func TestToggleActionsUsesNumericArmValues(t *testing.T) {
t.Parallel()

_, fake, camera := testServerWithCamera(t)
_, recorder, camera := testServerWithCamera(t)

fake.EXPECT().SimpleReq("++ssControlActions", url.Values{"arm": []string{"0"}}, camera.Number)
require.NoError(t, camera.ToggleActions(securityspy.CameraDisarm))

req, found := recorder.findLast("/++ssControlActions")
require.True(t, found)
require.Equal(t, "0", req.Query.Get("arm"))
require.Equal(t, "1", req.Query.Get("cameraNum"))
}
51 changes: 3 additions & 48 deletions cameras_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,7 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"golift.io/securityspy"
"golift.io/securityspy/mocks"
"golift.io/securityspy/server"
)

func TestUnmarshalXMLCameraSchedule(t *testing.T) {
Expand All @@ -27,21 +24,7 @@ func TestAll(t *testing.T) {
t.Parallel()
asert := assert.New(t)

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

secspyServer := securityspy.NewMust(
&server.Config{Username: "user", Password: "pass", URL: "http://127.0.0.1:5678", VerifySSL: true})
fake := mocks.NewMockAPI(mockCtrl)
secspyServer.API = fake

fake.EXPECT().GetXML(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Do(
func(_, _, v any) {
_ = xml.Unmarshal([]byte(testSystemInfo), &v)
},
)
require.NoError(t, secspyServer.Refresh(),
"there must no error when loading fake data") // load the fake testSystemInfo data.
secspyServer, _, _ := testServerWithCamera(t)

cams := secspyServer.Cameras.All()
asert.Len(cams, 2, "the data contains two cameras, two cameras must be returned")
Expand All @@ -51,21 +34,7 @@ func TestByNum(t *testing.T) {
t.Parallel()
asert := assert.New(t)

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

secspyServer := securityspy.NewMust(
&server.Config{Username: "user", Password: "pass", URL: "http://127.0.0.1:5678", VerifySSL: true})
fake := mocks.NewMockAPI(mockCtrl)
secspyServer.API = fake

fake.EXPECT().GetXML(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Do(
func(_, _, v any) {
_ = xml.Unmarshal([]byte(testSystemInfo), &v)
},
)
require.NoError(t, secspyServer.Refresh(),
"there must no error when loading fake data") // load the fake testSystemInfo data.
secspyServer, _, _ := testServerWithCamera(t)

cam := secspyServer.Cameras.ByNum(1)
asert.Equal("Porch", cam.Name, "camera 1 is Porch in the test data")
Expand All @@ -76,21 +45,7 @@ func TestByName(t *testing.T) {
t.Parallel()
asert := assert.New(t)

mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

secspyServer := securityspy.NewMust(
&server.Config{Username: "user", Password: "pass", URL: "http://127.0.0.1:5678", VerifySSL: true})
fake := mocks.NewMockAPI(mockCtrl)
secspyServer.API = fake

fake.EXPECT().GetXML(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Do(
func(_, _, v any) {
_ = xml.Unmarshal([]byte(testSystemInfo), &v)
},
)
require.NoError(t, secspyServer.Refresh(),
"there must no error when loading fake data") // load the fake testSystemInfo data.
secspyServer, _, _ := testServerWithCamera(t)

cam := secspyServer.Cameras.ByName("Porch")
asert.Equal(1, cam.Number, "camera 1 is Porch in the test data")
Expand Down
6 changes: 3 additions & 3 deletions events.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ func (e *Events) eventStreamSelector(ctx context.Context, refreshOnConfigChange
return
case EventConfigChange:
if refreshOnConfigChange {
e.serverRefresh()
e.serverRefresh(ctx)
}
}

Expand All @@ -313,8 +313,8 @@ func (e *Events) eventStreamSelector(ctx context.Context, refreshOnConfigChange
}
}

func (e *Events) serverRefresh() {
if err := e.server.Refresh(); err != nil {
func (e *Events) serverRefresh(ctx context.Context) {
if err := e.server.RefreshContext(ctx); err != nil {
e.custom(EventWatcherRefreshFail, -9997, -1, err.Error())

return
Expand Down
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ retract [v1.0.0, v2.0.2+incompatible]

require (
github.com/stretchr/testify v1.11.1
go.uber.org/mock v0.6.0
golift.io/ffmpeg v1.1.3
)

Expand Down
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golift.io/ffmpeg v1.1.3 h1:jTRKm6AeHpltrZ6fnUSdzfrxUwkyteoENqDaQGVVKes=
golift.io/ffmpeg v1.1.3/go.mod h1:NCYtkm9IDSiMezyGrbwoasEI+LsehmsCMUp5v/2Tkow=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
Loading
Loading