diff --git a/.github/renovate.json b/.github/renovate.json index fb0b963..aee303c 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,25 +1,22 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base", "default:automergeDigest"], + "extends": [ + "config:recommended", + "default:automergeDigest", + "helpers:pinGitHubActionDigestsToSemver" + ], + "minimumReleaseAge": "7 days", + "internalChecksFilter": "strict", "packageRules": [ { - "description": "Group Go stdlib / toolchain updates and automerge when tests pass", + "description": "Automerge non-major Go module updates when checks pass", "matchManagers": ["gomod"], - "matchDepNames": ["go"], - "matchDepTypes": ["golang", "toolchain"], - "groupName": "go", - "rangeStrategy": "bump", + "matchUpdateTypes": ["minor", "patch", "pin", "digest"], "automerge": true }, { - "description": "Group GitHub Actions updates and automerge when tests pass", + "description": "Automerge GitHub Actions updates, including majors, when checks pass", "matchManagers": ["github-actions"], - "groupName": "github-actions", - "automerge": true - }, - { - "matchDatasources": ["go", "github-releases", "github-tags"], - "matchUpdateTypes": ["minor", "patch", "pin", "digest"], "automerge": true } ] diff --git a/.github/workflows/codetests.yml b/.github/workflows/codetests.yml index 50da11a..f8e454c 100644 --- a/.github/workflows/codetests.yml +++ b/.github/workflows/codetests.yml @@ -3,12 +3,17 @@ on: pull_request: branches: - main - push: - branches: - - main permissions: contents: read jobs: + codespell: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: codespell-project/actions-codespell@e3258cca84ce02b0bb36a3d41f2c18f719a0cc1a # v2.4.3 + with: + skip: .git,go.sum + ignore_words_list: Medias # Runs `go test` against 3 operating systems. gotest: strategy: @@ -16,12 +21,12 @@ jobs: os: [macos, windows, ubuntu] runs-on: ${{ matrix.os }}-latest steps: - - uses: actions/checkout@v7 - - uses: actions/setup-go@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable - name: go-test - run: go test -race '-test.v' -covermode=atomic ./... + run: go test -race -covermode=atomic '-test.v' ./... # Runs golangci-lint on macos against freebsd and macos. golangci-darwin: strategy: @@ -32,14 +37,14 @@ jobs: env: GOOS: ${{ matrix.os }} steps: - - uses: actions/setup-go@v7 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: golangci-lint - uses: golangci/golangci-lint-action@v9 + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: v2.12 + version: v2.13 # Runs golangci-lint on linux against linux and windows. golangci-linux: strategy: @@ -50,11 +55,11 @@ jobs: env: GOOS: ${{ matrix.os }} steps: - - uses: actions/setup-go@v7 + - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version: stable - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: golangci-lint - uses: golangci/golangci-lint-action@v9 + uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0 with: - version: v2.12 + version: v2.13 diff --git a/.golangci.yml b/.golangci.yml index 8753653..cc7c33e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,30 +1,28 @@ -version: '2' +version: "2" linters: default: all disable: - - exhaustruct - - depguard - - wsl - - funcorder - - noinlineerr - - exhaustive + - wsl # deprecated. + - exhaustruct # deprecated. + - exhaustruct_v5 # camera and XML structs are filled a field at a time. + - gomodguard # deprecated. + - depguard # not useful. - nlreturn - - gomodguard - - goconst settings: + errcheck: + exclude-functions: + - (*os.File).Close + - (io.Closer).Close + exhaustive: + default-signifies-exhaustive: true gocritic: enable-all: true + disabled-checks: + - whyNoLint + - commentedOutCode settings: unnamedResult: checkExported: true - errcheck: - check-type-assertions: true - check-blank: false - disable-default-exclusions: false - exclude-functions: - - (*os.File).Close - - (io.Closer).Close - issues: max-issues-per-linter: 0 max-same-issues: 0 diff --git a/cameras.go b/cameras.go index 89a2e42..be7aef6 100644 --- a/cameras.go +++ b/cameras.go @@ -88,7 +88,8 @@ func (c *Camera) StreamVideo(ops *VidOps, length time.Duration, maxsize int64) ( // UseHTTP is not supported (returns ErrHTTPVideoUnsupported). // Set VidOps.VCodec to "h265" for HEVC cameras (see PreferredVCodec). func (c *Camera) SaveVideo(ops *VidOps, length time.Duration, maxsize int64, outputFile string) error { - if _, err := os.Stat(outputFile); !os.IsNotExist(err) { + _, statErr := os.Stat(outputFile) + if !os.IsNotExist(statErr) { return ErrPathExists } @@ -351,7 +352,8 @@ func (c *Camera) ToggleContinuous(arm CameraArmMode) error { params := make(url.Values) params.Set("arm", string(arm)) - if err := c.server.SimpleReq("++ssControlContinuous", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssControlContinuous", params, c.Number) + if err != nil { if errors.Is(err, server.ErrNotFound) { return ErrUnsupported } @@ -367,7 +369,8 @@ func (c *Camera) ToggleMotion(arm CameraArmMode) error { params := make(url.Values) params.Set("arm", string(arm)) - if err := c.server.SimpleReq("++ssControlMotionCapture", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssControlMotionCapture", params, c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } @@ -379,7 +382,8 @@ func (c *Camera) ToggleActions(arm CameraArmMode) error { params := make(url.Values) params.Set("arm", string(arm)) - if err := c.server.SimpleReq("++ssControlActions", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssControlActions", params, c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } @@ -389,7 +393,8 @@ func (c *Camera) ToggleActions(arm CameraArmMode) error { // TriggerMotion sets a camera as currently seeing motion. // Other actions likely occur because of this! func (c *Camera) TriggerMotion() error { - if err := c.server.SimpleReq("++triggermd", make(url.Values), c.Number); err != nil { + err := c.server.SimpleReq("++triggermd", make(url.Values), c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } @@ -497,7 +502,8 @@ func (c *Camera) SetSchedule(mode CameraMode, scheduleID int) error { params.Set("mode", string(mode)) params.Set("id", strconv.Itoa(scheduleID)) - if err := c.server.SimpleReq("++ssSetSchedule", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssSetSchedule", params, c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } @@ -512,7 +518,8 @@ func (c *Camera) SetScheduleOverride(mode CameraMode, overrideID int) error { params.Set("mode", string(mode)) params.Set("id", strconv.Itoa(overrideID)) - if err := c.server.SimpleReq("++ssSetOverride", params, c.Number); err != nil { + err := c.server.SimpleReq("++ssSetOverride", params, c.Number) + if err != nil { return fmt.Errorf("request failed: %w", err) } diff --git a/cameras_test.go b/cameras_test.go index ef7f9b5..18f90d3 100644 --- a/cameras_test.go +++ b/cameras_test.go @@ -11,13 +11,13 @@ import ( func TestUnmarshalXMLCameraSchedule(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) var s securityspy.CameraSchedule err := xml.Unmarshal([]byte("3"), &s) require.NoError(t, err, "valid data must not produce an error") - asert.Equal(3, s.ID, "the data was not unmarshalled properly") + check.Equal(3, s.ID, "the data was not unmarshalled properly") } // SS 5.5 keeps v5 width/height tags but also emits video-format (a v6-era field). @@ -54,40 +54,40 @@ func TestUnmarshalXMLCameraSS55WithVideoFormat(t *testing.T) { func TestAll(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) secspyServer, _, _ := testServerWithCamera(t) cams := secspyServer.Cameras.All() - asert.Len(cams, 2, "the data contains two cameras, two cameras must be returned") + check.Len(cams, 2, "the data contains two cameras, two cameras must be returned") } func TestByNum(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) secspyServer, _, _ := testServerWithCamera(t) cam := secspyServer.Cameras.ByNum(2) - asert.Equal("Porch", cam.Name, "camera 2 is Porch in the v6 test data") + check.Equal("Porch", cam.Name, "camera 2 is Porch in the v6 test data") require.Nil(t, secspyServer.Cameras.ByNum(99), "a non-existent camera must return nil") } func TestByName(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) secspyServer, _, _ := testServerWithCamera(t) cam := secspyServer.Cameras.ByName("Porch") - asert.Equal(2, cam.Number, "camera 2 is Porch in the v6 test data") + check.Equal(2, cam.Number, "camera 2 is Porch in the v6 test data") require.Nil(t, secspyServer.Cameras.ByName("not here"), "a non-existent camera must return nil") cam = secspyServer.Cameras.ByName("porch2") require.Nil(t, cam, "there is no camera named porch2") cam = secspyServer.Cameras.ByName("porch") - asert.Equal(2, cam.Number, "camera 2 is Porch in the v6 test data") + check.Equal(2, cam.Number, "camera 2 is Porch in the v6 test data") require.Nil(t, secspyServer.Cameras.ByName("not here"), "a non-existent camera must return nil") } diff --git a/cameras_types.go b/cameras_types.go index fdadb05..198878b 100644 --- a/cameras_types.go +++ b/cameras_types.go @@ -105,7 +105,8 @@ type CameraSchedule struct { // UnmarshalXML stores a schedule ID into a CameraSchedule type. // This isn't a method you should ever call directly; it is only used during data initialization. func (bit *CameraSchedule) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - if err := d.DecodeElement(&bit.ID, &start); err != nil { + err := d.DecodeElement(&bit.ID, &start) + if err != nil { return fmt.Errorf("decoding xml: %w", err) } @@ -340,7 +341,9 @@ type cameraXML struct { //nolint:funlen // dual schema mapping func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var raw cameraXML - if err := d.DecodeElement(&raw, &start); err != nil { + + err := d.DecodeElement(&raw, &start) + if err != nil { return fmt.Errorf("decoding camera xml: %w", err) } diff --git a/events.go b/events.go index 756d88c..3740381 100644 --- a/events.go +++ b/events.go @@ -325,7 +325,8 @@ func (e *Events) eventStreamSelector(ctx context.Context, refreshOnConfigChange } func (e *Events) serverRefresh(ctx context.Context) { - if err := e.server.RefreshContext(ctx); err != nil { + err := e.server.RefreshContext(ctx) + if err != nil { e.custom(EventWatcherRefreshFail, -9997, -1, err.Error()) return @@ -380,13 +381,15 @@ func (e *Events) UnmarshalEvent(text string) *Event { eventTime = fmt.Sprintf("%v%+03.0f", parts[0], gmtOffset) //nolint:gosmopolitan // The event stream uses the system's local time. - if newEvent.When, err = time.ParseInLocation(EventTimeFormat+"-07", eventTime, time.Local); err != nil { + newEvent.When, err = time.ParseInLocation(EventTimeFormat+"-07", eventTime, time.Local) + if err != nil { newEvent.When = time.Now() newEvent.Errors = append(newEvent.Errors, ErrDateParseFail) } // Parse the ID - if newEvent.ID, err = strconv.Atoi(parts[1]); err != nil { + newEvent.ID, err = strconv.Atoi(parts[1]) + if err != nil { newEvent.ID = BadID newEvent.Errors = append(newEvent.Errors, ErrIDParseFail) } @@ -395,7 +398,8 @@ func (e *Events) UnmarshalEvent(text string) *Event { parts[2] = strings.TrimPrefix(parts[2], "CAM") if cams := e.server.GetCameras(); parts[2] != "X" && cams != nil { - if cameraNum, err := strconv.Atoi(parts[2]); err != nil { + cameraNum, err := strconv.Atoi(parts[2]) + if err != nil { newEvent.Errors = append(newEvent.Errors, ErrCAMParseFail) } else if newEvent.Camera = cams.ByNum(cameraNum); newEvent.Camera == nil { newEvent.Errors = append(newEvent.Errors, ErrCAMMissing) diff --git a/files.go b/files.go index e85d296..baec330 100644 --- a/files.go +++ b/files.go @@ -17,8 +17,8 @@ import ( const ( // DownloadDateFormat is the format the SecuritySpy ++download method accepts. // This matches the ++download inputs AND the folder names files are saved into. - // The file1/file2 inputs this gets passed into are actually undocuemnted and were - // created specifically for programmtic SDK access (ie. this library). + // The file1/file2 inputs this gets passed into are actually undocumented and were + // created specifically for programmatic SDK access (ie. this library). DownloadDateFormat = "2006-01-02" // FileDateFormat is an arbitrary date format used for saved files; we hope doesn't change. // This is used in the actual name of files that are saved. No where else. @@ -50,7 +50,7 @@ type fileFeed struct { Entries []*File `xml:"entry"` // List of File pointers } -// File represents a saved media file. This is all the data retreived from +// File represents a saved media file. This is all the data retrieved from // the ++download method for a particular file. Contains a camera interface // for the camera that created the file. All of the Files type methods return this type. type File struct { @@ -110,15 +110,25 @@ func (f *Files) GetFile(name string) (*File, error) { } cams := f.server.GetCameras() - if fileExtSplit := strings.Split(name, "."); len(fileExtSplit) != fileParts { + fileExtSplit := strings.Split(name, ".") + if len(fileExtSplit) != fileParts { return file, ErrInvalidName - } else if nameDateSplit := strings.Split(fileExtSplit[0], " "); len(fileExtSplit) < fileParts { - return file, ErrInvalidName - } else if file.Updated, err = time.Parse(FileDateFormat, nameDateSplit[0]); err != nil { + } + + nameDateSplit := strings.Split(fileExtSplit[0], " ") + + file.Updated, err = time.Parse(FileDateFormat, nameDateSplit[0]) + if err != nil { return file, ErrInvalidName - } else if file.Camera = cams.ByName(nameDateSplit[len(nameDateSplit)-1]); file.Camera == nil { + } + + file.Camera = cams.ByName(nameDateSplit[len(nameDateSplit)-1]) + if file.Camera == nil { return file, ErrCAMMissing - } else if file.Link.Type = "video/quicktime"; fileExtSplit[1] == "jpg" { + } + + file.Link.Type = "video/quicktime" + if fileExtSplit[1] == "jpg" { file.Link.Type = "image/jpeg" } @@ -134,7 +144,8 @@ func (f *Files) GetFile(name string) (*File, error) { // Save downloads a saved media File from SecuritySpy and saves it to a local file. // Returns an error if path exists. func (f *File) Save(path string) (int64, error) { - if _, err := os.Stat(path); !os.IsNotExist(err) { + _, statErr := os.Stat(path) + if !os.IsNotExist(statErr) { return 0, ErrPathExists } @@ -187,7 +198,8 @@ func (f *Files) getFiles(cameraNums []int, start, end time.Time, fileTypes, cont params = makeFilesParams(cameraNums, start, end, fileTypes, continuation) ) - if err := f.server.GetXML("++download", params, &feed); err != nil { + err := f.server.GetXML("++download", params, &feed) + if err != nil { return nil, fmt.Errorf("getting download: %w", err) } @@ -214,7 +226,7 @@ func (f *Files) getFiles(cameraNums []int, start, end time.Time, fileTypes, cont return entries, nil } -// makeFilesParams makes the url Values for a file retreival. +// makeFilesParams makes the url Values for a file retrieval. func makeFilesParams(cameraNums []int, from, to time.Time, fileTypes, continuation string) url.Values { params := make(url.Values) params.Set("results", "1000") diff --git a/internal/rtspclip/clip.go b/internal/rtspclip/clip.go index d8a8b81..3c8d419 100644 --- a/internal/rtspclip/clip.go +++ b/internal/rtspclip/clip.go @@ -89,7 +89,8 @@ func SaveMP4(ctx context.Context, rtspURL, path string, opts Options) (*Result, return nil, ErrBadPath } - if _, err := os.Stat(path); err == nil { + _, statErr := os.Stat(path) + if statErr == nil { return nil, os.ErrExist } @@ -147,7 +148,8 @@ type cancelReadCloser struct { func (c *cancelReadCloser) Close() error { c.cancel() - if err := c.ReadCloser.Close(); err != nil { + err := c.ReadCloser.Close() + if err != nil { return fmt.Errorf("close stream: %w", err) } @@ -162,7 +164,8 @@ func capture(ctx context.Context, rtspURL string, opts Options, writer io.Writer client := newRTSPClient(parsed, opts.InsecureSkipVerify) - if err := client.Start(); err != nil { + err = client.Start() + if err != nil { return nil, fmt.Errorf("rtsp start: %w", err) } @@ -186,22 +189,26 @@ func capture(ctx context.Context, rtspURL string, opts Options, writer io.Writer state := &captureState{} stop := makeStopper(state, cancel, client) - if err := client.SetupAll(session.BaseURL, session.Medias); err != nil { + err = client.SetupAll(session.BaseURL, session.Medias) + if err != nil { return nil, fmt.Errorf("rtsp setup: %w", err) } attachVideo(client, tracks, mux, state, opts, stop) attachAudio(client, tracks, mux, state, opts, stop) - if _, err := client.Play(nil); err != nil { + _, err = client.Play(nil) + if err != nil { return nil, fmt.Errorf("rtsp play: %w", err) } - if err := waitCapture(captureCtx, client, state, stop); err != nil { + err = waitCapture(captureCtx, client, state, stop) + if err != nil { return nil, err } - if err := mux.close(); err != nil { + err = mux.close() + if err != nil { return nil, err } @@ -620,7 +627,8 @@ func (m *fmp4Muxer) writeVideo(accessUnit [][]byte, pts int64) (int, error) { return 0, nil } - if err := m.ensureVideoStarted(idr, pts); err != nil { + err := m.ensureVideoStarted(idr, pts) + if err != nil { return 0, err } @@ -674,7 +682,8 @@ func (m *fmp4Muxer) afterVideoSample() error { return nil } - if err := m.maybeInitProgressive(); err != nil { + err := m.maybeInitProgressive() + if err != nil { return err } @@ -787,8 +796,11 @@ func (m *fmp4Muxer) writeInit(includeAudio bool) error { if err != nil { return fmt.Errorf("hevc descriptor: %w", err) } - } else if err := init.Moov.Trak.SetAVCDescriptor("avc1", [][]byte{m.sps}, [][]byte{m.pps}, true); err != nil { - return fmt.Errorf("avc descriptor: %w", err) + } else { + err := init.Moov.Trak.SetAVCDescriptor("avc1", [][]byte{m.sps}, [][]byte{m.pps}, true) + if err != nil { + return fmt.Errorf("avc descriptor: %w", err) + } } if includeAudio && m.aacFormat != nil && m.aacFormat.Config != nil && m.audioRate > 0 { @@ -797,12 +809,14 @@ func (m *fmp4Muxer) writeInit(includeAudio bool) error { traks := init.Moov.Traks audioTrak := traks[len(traks)-1] - if err := setMPEG4AudioDescriptor(audioTrak, m.aacFormat.Config); err != nil { + err := setMPEG4AudioDescriptor(audioTrak, m.aacFormat.Config) + if err != nil { return err } } - if err := init.Encode(m.w); err != nil { + err := init.Encode(m.w) + if err != nil { return fmt.Errorf("write init: %w", err) } @@ -844,7 +858,9 @@ func (m *fmp4Muxer) maybeInitProgressive() error { } m.includeAudio = len(m.audioSamples) > 0 && m.aacFormat != nil && m.aacFormat.Config != nil - if err := m.writeInit(m.includeAudio); err != nil { + + err := m.writeInit(m.includeAudio) + if err != nil { return err } @@ -881,20 +897,23 @@ func (m *fmp4Muxer) flushFragment() error { seg.AddFragment(frag) for _, sample := range m.videoSamples { - if err := frag.AddFullSampleToTrack(sample, videoTrackID); err != nil { + err := frag.AddFullSampleToTrack(sample, videoTrackID) + if err != nil { return fmt.Errorf("add video sample: %w", err) } } if m.includeAudio { for _, sample := range m.audioSamples { - if err := frag.AddFullSampleToTrack(sample, audioTrackID); err != nil { + err := frag.AddFullSampleToTrack(sample, audioTrackID) + if err != nil { return fmt.Errorf("add audio sample: %w", err) } } } - if err := seg.Encode(m.w); err != nil { + err = seg.Encode(m.w) + if err != nil { return fmt.Errorf("encode segment: %w", err) } @@ -914,7 +933,8 @@ func (m *fmp4Muxer) close() error { } if m.progressive { - if err := m.maybeInitProgressive(); err != nil { + err := m.maybeInitProgressive() + if err != nil { return err } @@ -930,7 +950,8 @@ func (m *fmp4Muxer) close() error { m.hadAudio = true } - if err := m.writeInit(includeAudio); err != nil { + err := m.writeInit(includeAudio) + if err != nil { return err } diff --git a/internal/rtspclip/clip_test.go b/internal/rtspclip/clip_test.go index 350eea8..d14a47d 100644 --- a/internal/rtspclip/clip_test.go +++ b/internal/rtspclip/clip_test.go @@ -46,7 +46,6 @@ func TestMonoAAC64kASC(t *testing.T) { cfg := mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 64000, - ChannelCount: 1, ChannelConfig: 1, } asc, err := cfg.Marshal() @@ -61,7 +60,6 @@ func TestHighSampleRateASCStillMarshals(t *testing.T) { cfg := mpeg4audio.AudioSpecificConfig{ Type: mpeg4audio.ObjectTypeAACLC, SampleRate: 96000, - ChannelCount: 2, ChannelConfig: 2, } asc, err := cfg.Marshal() diff --git a/ptz.go b/ptz.go index b419922..d6326c3 100644 --- a/ptz.go +++ b/ptz.go @@ -198,24 +198,11 @@ func (z *PTZ) Stop() error { return z.ptzReq(ptzCommandStop) } -/* INTERFACE HELPER METHODS FOLLOW */ - -// ptzReq wraps all the ptz-specific calls. -func (z *PTZ) ptzReq(command ptzCommand) error { - params := make(url.Values) - params.Set("command", strconv.Itoa(int(command))) - - if err := z.camera.server.SimpleReq("++ptz/command", params, z.camera.Number); err != nil { - return fmt.Errorf("ptz failed: %w", err) - } - - return nil -} - -// UnmarshalXML method converts ptzCapbilities bitmask from an XML payload into true/false abilities. +// UnmarshalXML method converts ptzCapabilities bitmask from an XML payload into true/false abilities. // This isn't a method you should ever call directly; it is only used during data initialization. func (z *PTZ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { - if err := d.DecodeElement(&z.rawCaps, &start); err != nil { + err := d.DecodeElement(&z.rawCaps, &start) + if err != nil { return fmt.Errorf("ptz caps: %w", err) } @@ -228,3 +215,16 @@ func (z *PTZ) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } + +// ptzReq wraps all the ptz-specific calls. +func (z *PTZ) ptzReq(command ptzCommand) error { + params := make(url.Values) + params.Set("command", strconv.Itoa(int(command))) + + err := z.camera.server.SimpleReq("++ptz/command", params, z.camera.Number) + if err != nil { + return fmt.Errorf("ptz failed: %w", err) + } + + return nil +} diff --git a/schedules.go b/schedules.go index a76c618..20bbd2d 100644 --- a/schedules.go +++ b/schedules.go @@ -46,7 +46,8 @@ func (m *ScheduleContainer) UnmarshalXML(decoder *xml.Decoder, start xml.StartEl switch xmlToken := token.(type) { case xml.StartElement: - if err = decoder.DecodeElement(&schedule, &xmlToken); err != nil { + err = decoder.DecodeElement(&schedule, &xmlToken) + if err != nil { return fmt.Errorf("XML decode: %w", err) } @@ -65,7 +66,8 @@ func (s *Server) SetSchedulePreset(presetID int) error { params := make(url.Values) params.Set("id", strconv.Itoa(presetID)) - if err := s.SimpleReq("++ssSetPreset", params, -1); err != nil { + err := s.SimpleReq("++ssSetPreset", params, -1) + if err != nil { return fmt.Errorf("http request: %w", err) } diff --git a/schedules_test.go b/schedules_test.go index f37b011..69ebb0e 100644 --- a/schedules_test.go +++ b/schedules_test.go @@ -24,14 +24,14 @@ func TestSetSchedulePreset(t *testing.T) { func TestUnmarshalXMLscheduleContainer(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) var schedule securityspy.ScheduleContainer err := xml.Unmarshal([]byte(testScheduleList), &schedule) require.NoError(t, err, "valid data must not produce an error") - asert.Equal("Armed 24/7", schedule[1], "the scheduleContainer data did not unmarshal properly") - asert.Len(schedule, 6) + check.Equal("Armed 24/7", schedule[1], "the scheduleContainer data did not unmarshal properly") + check.Len(schedule, 6) err = xml.Unmarshal([]byte(""), &schedule) require.Error(t, err, "invalid data must produce an error") diff --git a/securityspy.go b/securityspy.go index 69626bd..9794603 100644 --- a/securityspy.go +++ b/securityspy.go @@ -21,9 +21,9 @@ func New(c *server.Config) (*Server, error) { return s, s.RefreshContext(context.Background()) //nolint:gocritic } -// NewMust returns an iterface to interact with SecuritySpy. +// NewMust returns an interface to interact with SecuritySpy. // This does not attempt to connect to SecuritySpy first. -// You must call s.Refresh() before attempting to access other datas. +// You must call s.Refresh() before attempting to access other data. func NewMust(config *server.Config) *Server { if !strings.HasSuffix(config.URL, "/") { config.URL += "/" @@ -169,7 +169,8 @@ func (s *Server) GetScripts() ([]string, error) { Names []string `xml:"name"` } - if err := s.GetXML("++scripts", nil, &val); err != nil { + err := s.GetXML("++scripts", nil, &val) + if err != nil { return nil, fmt.Errorf("getting scripts: %w", err) } @@ -183,7 +184,8 @@ func (s *Server) GetSounds() ([]string, error) { Names []string `xml:"name"` } - if err := s.GetXML("++sounds", nil, &val); err != nil { + err := s.GetXML("++sounds", nil, &val) + if err != nil { return nil, fmt.Errorf("getting sounds: %w", err) } diff --git a/securityspy_test.go b/securityspy_test.go index a9fda0a..f5f7f39 100644 --- a/securityspy_test.go +++ b/securityspy_test.go @@ -23,14 +23,14 @@ const ( func TestGetServer(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) URL := "http://127.0.0.1:5678" user := "user123" pass := "pass456" secspyServer, err := securityspy.New(&server.Config{Username: user, Password: pass, URL: URL, VerifySSL: true}) require.Error(t, err, "there is no server at the address provided so an error must exist") - asert.NotNil(secspyServer, "server must not be nil. even wiuth an error it must be returned") + check.NotNil(secspyServer, "server must not be nil. even with an error it must be returned") if !strings.Contains(err.Error(), "target machine actively refused it") && !strings.Contains(err.Error(), "connection refused") { @@ -62,26 +62,26 @@ func TestRefresh(t *testing.T) { })) defer fakeServer.Close() - asert := assert.New(t) + check := assert.New(t) secspyServer := securityspy.NewMust( &server.Config{Username: userStr, Password: passStr, URL: fakeServer.URL + "/", VerifySSL: false}) require.NoError(t, secspyServer.Refresh(), "an error must not be returned while testing with valid XML") // Make sure Refresh() did all the things it is supposed to do. - asert.WithinDuration(time.Now(), secspyServer.Info.Refreshed, time.Second, + check.WithinDuration(time.Now(), secspyServer.Info.Refreshed, time.Second, "Refreshed field must be updated by Refresh() method") // Test that the data was unmarshalled properly. // These tests assume the test data does not change. - asert.Equal("SecuritySpy", secspyServer.Info.Name, "the server's name was not properly unmarshalled") - asert.Equal("2019-02-10T15:53:23", secspyServer.Info.CurrentTime.Format("2006-01-02T15:04:05"), + check.Equal("SecuritySpy", secspyServer.Info.Name, "the server's name was not properly unmarshalled") + check.Equal("2019-02-10T15:53:23", secspyServer.Info.CurrentTime.Format("2006-01-02T15:04:05"), "the server's current time was not properly unmarshalled") - asert.Equal(2304, secspyServer.Cameras.ByNum(1).Width, "camera info was not properly unmarshalled") - asert.Equal("Road", secspyServer.Cameras.ByNum(2).Name, "camera info was not properly unmarshalled") - asert.Equal("Unarmed 24/7", secspyServer.Info.ServerSchedules[0], "schedule info was not properly unmarshalled") - asert.Equal("None", secspyServer.Info.ScheduleOverrides[0], "schedule override info was not properly unmarshalled") - asert.Equal("MyFirstPreset", secspyServer.Info.SchedulePresets[1930238093], + check.Equal(2304, secspyServer.Cameras.ByNum(1).Width, "camera info was not properly unmarshalled") + check.Equal("Road", secspyServer.Cameras.ByNum(2).Name, "camera info was not properly unmarshalled") + check.Equal("Unarmed 24/7", secspyServer.Info.ServerSchedules[0], "schedule info was not properly unmarshalled") + check.Equal("None", secspyServer.Info.ScheduleOverrides[0], "schedule override info was not properly unmarshalled") + check.Equal("MyFirstPreset", secspyServer.Info.SchedulePresets[1930238093], "schedule preset info was not properly unmarshalled") // second call returns an HTTP error response. @@ -111,14 +111,14 @@ func TestGetSounds(t *testing.T) { //nolint:dupl // it just looks like a duplica })) defer fakeServer.Close() - asert := assert.New(t) + check := assert.New(t) secspyServer := securityspy.NewMust( &server.Config{Username: userStr, Password: passStr, URL: fakeServer.URL + "/", VerifySSL: false}) sounds, err := secspyServer.GetSounds() require.NoError(t, err, "the method must not return an error when given valid XML to unmarshal") - asert.Len(sounds, 20, "all 20 sounds must exist in the slice") - asert.Equal("Beeps.aif", sounds[0], "the sound files were not properly unmarhsalled") + check.Len(sounds, 20, "all 20 sounds must exist in the slice") + check.Equal("Beeps.aif", sounds[0], "the sound files were not properly unmarshalled") _, err = secspyServer.GetSounds() require.Error(t, err) @@ -147,14 +147,14 @@ func TestGetScripts(t *testing.T) { //nolint:dupl // it just looks like a duplic })) defer fakeServer.Close() - asert := assert.New(t) + check := assert.New(t) secspyServer := securityspy.NewMust( &server.Config{Username: userStr, Password: passStr, URL: fakeServer.URL + "/", VerifySSL: false}) scripts, err := secspyServer.GetScripts() require.NoError(t, err, "the method must not return an error when given valid XML to unmarshal") - asert.Len(scripts, 16, "all 16 scripts must exist in the slice") - asert.Equal("Web-i Activate Relay 1.scpt", scripts[0], "the script files were not properly unmarhsalled") + check.Len(scripts, 16, "all 16 scripts must exist in the slice") + check.Equal("Web-i Activate Relay 1.scpt", scripts[0], "the script files were not properly unmarshalled") _, err = secspyServer.GetScripts() require.Error(t, err) @@ -163,7 +163,7 @@ func TestGetScripts(t *testing.T) { //nolint:dupl // it just looks like a duplic func TestUnmarshalXMLYesNoBool(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) good := []string{"true", "yes", "1", "armed", "active", "enabled"} fail := []string{"anything", "else", "returns", "false", "including", "no", "0", "disarmed", "inactive", "disabled"} @@ -171,38 +171,38 @@ func TestUnmarshalXMLYesNoBool(t *testing.T) { for _, val := range good { require.NoError(t, xml.Unmarshal([]byte(""+val+""), &bit), "unmarshalling must not produce an error") - asert.True(bit.Val, "the value must unmarshal to true") - asert.Equal(val, bit.Txt, "the value was not unmarshalled correctly") + check.True(bit.Val, "the value must unmarshal to true") + check.Equal(val, bit.Txt, "the value was not unmarshalled correctly") } for _, val := range fail { require.NoError(t, xml.Unmarshal([]byte(""+val+""), &bit), "unmarshalling must not produce an error") - asert.False(bit.Val, "the value must unmarshal to false") - asert.Equal(val, bit.Txt, "the value was not unmarshalled correctly") + check.False(bit.Val, "the value must unmarshal to false") + check.Equal(val, bit.Txt, "the value was not unmarshalled correctly") } } func TestUnmarshalXMLDuration(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) good := []string{"1", "20", "300", "4000", "50000", "666666"} var bit securityspy.Duration for _, val := range good { require.NoError(t, xml.Unmarshal([]byte(""+val+""), &bit), "unmarshalling must not produce an error") - asert.Equal(val, bit.Val, "the value was not unmarshalled correctly") + check.Equal(val, bit.Val, "the value was not unmarshalled correctly") num, err := strconv.ParseFloat(val, 64) require.NoError(t, err, "must not be an error parsing test numbers") - asert.InDelta(num, bit.Seconds(), 0.000001, "the value was not unmarshalled correctly") - asert.Equal(val, bit.Val, "the value was not unmarshalled correctly") + check.InDelta(num, bit.Seconds(), 0.000001, "the value was not unmarshalled correctly") + check.Equal(val, bit.Val, "the value was not unmarshalled correctly") } // Test empty value. require.NoError(t, xml.Unmarshal([]byte(""), &bit), "unmarshalling must not produce an error") - asert.Empty(bit.Val, "the value was not unmarshalled correctly") - asert.Equal(int64(-1), bit.Nanoseconds(), "an empty value must produce -1 nano second.") + check.Empty(bit.Val, "the value was not unmarshalled correctly") + check.Equal(int64(-1), bit.Nanoseconds(), "an empty value must produce -1 nano second.") } func TestRefreshHandlesNilPTZAndMissingSchedules(t *testing.T) { diff --git a/server/server.go b/server/server.go index cbc261b..06d4119 100644 --- a/server/server.go +++ b/server/server.go @@ -244,14 +244,15 @@ func (s *Config) GetXMLContext(ctx context.Context, apiPath string, params url.V s.Username, s.URL+apiPath, resp.StatusCode, resp.Status, err, string(body)) } - if err = xml.NewDecoder(resp.Body).Decode(val); err != nil { + err = xml.NewDecoder(resp.Body).Decode(val) + if err != nil { return fmt.Errorf("reading body: %w", err) } return nil } -// SimpleReq performes HTTP req, checks for OK at end of output. +// SimpleReq performs HTTP req, checks for OK at end of output. func (s *Config) SimpleReq(apiURI string, params url.Values, cameraNum int) error { ctx, cancel := context.WithTimeout(context.Background(), s.TimeoutDur()) defer cancel() @@ -259,7 +260,7 @@ func (s *Config) SimpleReq(apiURI string, params url.Values, cameraNum int) erro return s.SimpleReqContext(ctx, apiURI, params, cameraNum) } -// SimpleReqContext performes HTTP req, checks for OK at end of output. +// SimpleReqContext performs HTTP req, checks for OK at end of output. func (s *Config) SimpleReqContext(ctx context.Context, apiURI string, params url.Values, cameraNum int) error { if cameraNum >= 0 { params.Set("cameraNum", strconv.Itoa(cameraNum)) diff --git a/server/server_test.go b/server/server_test.go index 07c3b2d..e2dbe78 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -25,7 +25,7 @@ const ( func TestGet(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) config := &server.Config{ Username: userStr, Password: passStr, @@ -34,13 +34,13 @@ func TestGet(t *testing.T) { Timeout: server.Duration{time.Second}, } handler := http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { - asert.Equal("xml", req.URL.Query().Get("format"), "format parameter was not added") - asert.Equal(config.Password, req.URL.Query().Get("auth"), "auth parameter was not added") - asert.Equal("application/xml", req.Header.Get("Accept"), "accept header is not correct") + check.Equal("xml", req.URL.Query().Get("format"), "format parameter was not added") + check.Equal(config.Password, req.URL.Query().Get("auth"), "auth parameter was not added") + check.Equal("application/xml", req.Header.Get("Accept"), "accept header is not correct") _, err := resp.Write([]byte("request OK")) - asert.NoError(err, "the fake server must return an error writing to the client") - asert.Equal(config.URL, "http://"+req.Host+"/", "the host was not set correctly in the request") + check.NoError(err, "the fake server must return an error writing to the client") + check.Equal(config.URL, "http://"+req.Host+"/", "the host was not set correctly in the request") }) httpClient, fakeServer := testingHTTPClient(handler) @@ -53,17 +53,17 @@ func TestGet(t *testing.T) { if err == nil { defer resp.Body.Close() - asert.Equal(http.StatusOK, resp.StatusCode, "the server must return a 200 response code") + check.Equal(http.StatusOK, resp.StatusCode, "the server must return a 200 response code") body, err := io.ReadAll(resp.Body) require.NoError(t, err, "must not be an error reading the response body") - asert.Equal("request OK", string(body), "wrong data was returned from the server") + check.Equal("request OK", string(body), "wrong data was returned from the server") } } func TestPostAudioContentType(t *testing.T) { t.Parallel() - asert := assert.New(t) + check := assert.New(t) config := &server.Config{ Username: userStr, Password: passStr, @@ -86,7 +86,7 @@ func TestPostAudioContentType(t *testing.T) { } _, err := resp.Write([]byte("OK")) - asert.NoError(err) + check.NoError(err) }) httpClient, fakeServer := testingHTTPClient(handler) @@ -96,14 +96,14 @@ func TestPostAudioContentType(t *testing.T) { body, err := config.Post("++audio", nil, io.NopCloser(strings.NewReader("audio-data"))) require.NoError(t, err) - asert.Equal("OK", string(body)) + check.Equal("OK", string(body)) body, err = config.Post("++other", nil, io.NopCloser(strings.NewReader("other-data"))) require.NoError(t, err) - asert.Equal("OK", string(body)) + check.Equal("OK", string(body)) - asert.Equal("audio/g711-ulaw", audioHeader) - asert.Empty(otherHeader) + check.Equal("audio/g711-ulaw", audioHeader) + check.Empty(otherHeader) } func TestGetXMLStatusAndDecodeErrors(t *testing.T) { diff --git a/settings.go b/settings.go index 5d25b58..3bd1b46 100644 --- a/settings.go +++ b/settings.go @@ -13,7 +13,9 @@ var ErrCameraNumRequired = errors.New("cameraNum required") // GetGeneralSettings fetches ++settings-general. func (s *Server) GetGeneralSettings() (*GeneralSettings, error) { var val GeneralSettings - if err := s.GetXML("++settings-general", nil, &val); err != nil { + + err := s.GetXML("++settings-general", nil, &val) + if err != nil { return nil, fmt.Errorf("getting general settings: %w", err) } @@ -22,7 +24,8 @@ func (s *Server) GetGeneralSettings() (*GeneralSettings, error) { // SetGeneralSettings posts form fields to ++settings-general (partial update). func (s *Server) SetGeneralSettings(form url.Values) error { - if err := s.PostForm("++settings-general", form); err != nil { + err := s.PostForm("++settings-general", form) + if err != nil { return fmt.Errorf("setting general settings: %w", err) } @@ -32,7 +35,9 @@ func (s *Server) SetGeneralSettings(form url.Values) error { // GetDisplaySettings fetches ++settings-display. func (s *Server) GetDisplaySettings() (*DisplaySettings, error) { var val DisplaySettings - if err := s.GetXML("++settings-display", nil, &val); err != nil { + + err := s.GetXML("++settings-display", nil, &val) + if err != nil { return nil, fmt.Errorf("getting display settings: %w", err) } @@ -41,7 +46,8 @@ func (s *Server) GetDisplaySettings() (*DisplaySettings, error) { // SetDisplaySettings posts form fields to ++settings-display (partial update). func (s *Server) SetDisplaySettings(form url.Values) error { - if err := s.PostForm("++settings-display", form); err != nil { + err := s.PostForm("++settings-display", form) + if err != nil { return fmt.Errorf("setting display settings: %w", err) } @@ -51,7 +57,9 @@ func (s *Server) SetDisplaySettings(form url.Values) error { // GetStorageSettings fetches ++settings-storage. func (s *Server) GetStorageSettings() (*StorageSettings, error) { var val StorageSettings - if err := s.GetXML("++settings-storage", nil, &val); err != nil { + + err := s.GetXML("++settings-storage", nil, &val) + if err != nil { return nil, fmt.Errorf("getting storage settings: %w", err) } @@ -60,7 +68,8 @@ func (s *Server) GetStorageSettings() (*StorageSettings, error) { // SetStorageSettings posts form fields to ++settings-storage (partial update). func (s *Server) SetStorageSettings(form url.Values) error { - if err := s.PostForm("++settings-storage", form); err != nil { + err := s.PostForm("++settings-storage", form) + if err != nil { return fmt.Errorf("setting storage settings: %w", err) } @@ -70,7 +79,9 @@ func (s *Server) SetStorageSettings(form url.Values) error { // GetCompressionSettings fetches ++settings-compression. func (s *Server) GetCompressionSettings() (*CompressionSettings, error) { var val CompressionSettings - if err := s.GetXML("++settings-compression", nil, &val); err != nil { + + err := s.GetXML("++settings-compression", nil, &val) + if err != nil { return nil, fmt.Errorf("getting compression settings: %w", err) } @@ -79,7 +90,8 @@ func (s *Server) GetCompressionSettings() (*CompressionSettings, error) { // SetCompressionSettings posts form fields to ++settings-compression (partial update). func (s *Server) SetCompressionSettings(form url.Values) error { - if err := s.PostForm("++settings-compression", form); err != nil { + err := s.PostForm("++settings-compression", form) + if err != nil { return fmt.Errorf("setting compression settings: %w", err) } @@ -89,7 +101,9 @@ func (s *Server) SetCompressionSettings(form url.Values) error { // GetEmailSettings fetches ++settings-email. func (s *Server) GetEmailSettings() (*EmailSettings, error) { var val EmailSettings - if err := s.GetXML("++settings-email", nil, &val); err != nil { + + err := s.GetXML("++settings-email", nil, &val) + if err != nil { return nil, fmt.Errorf("getting email settings: %w", err) } @@ -98,7 +112,8 @@ func (s *Server) GetEmailSettings() (*EmailSettings, error) { // SetEmailSettings posts form fields to ++settings-email (partial update). func (s *Server) SetEmailSettings(form url.Values) error { - if err := s.PostForm("++settings-email", form); err != nil { + err := s.PostForm("++settings-email", form) + if err != nil { return fmt.Errorf("setting email settings: %w", err) } @@ -108,7 +123,9 @@ func (s *Server) SetEmailSettings(form url.Values) error { // GetWebSettings fetches ++settings-web. func (s *Server) GetWebSettings() (*WebSettings, error) { var val WebSettings - if err := s.GetXML("++settings-web", nil, &val); err != nil { + + err := s.GetXML("++settings-web", nil, &val) + if err != nil { return nil, fmt.Errorf("getting web settings: %w", err) } @@ -117,7 +134,8 @@ func (s *Server) GetWebSettings() (*WebSettings, error) { // SetWebSettings posts form fields to ++settings-web (partial update). func (s *Server) SetWebSettings(form url.Values) error { - if err := s.PostForm("++settings-web", form); err != nil { + err := s.PostForm("++settings-web", form) + if err != nil { return fmt.Errorf("setting web settings: %w", err) } @@ -130,7 +148,9 @@ func (s *Server) GetCameraSettings(cameraNum int) (*CameraSettings, error) { params.Set("cameraNum", strconv.Itoa(cameraNum)) var val CameraSettings - if err := s.GetXML("++settings-cameras", params, &val); err != nil { + + err := s.GetXML("++settings-cameras", params, &val) + if err != nil { return nil, fmt.Errorf("getting camera settings: %w", err) } @@ -144,7 +164,8 @@ func (s *Server) SetCameraSettings(form url.Values) error { return ErrCameraNumRequired } - if err := s.PostForm("++settings-cameras", form); err != nil { + err := s.PostForm("++settings-cameras", form) + if err != nil { return fmt.Errorf("setting camera settings: %w", err) }