Repository: edgexfoundry/device-sdk-go
Pull request #1480 (2023-07-19 17:59:59)

feat: better error handling when running in hybrid mode but common config is missing
with better error message handling from the go-mod-bootstrap PR, there is some logic needs to be fixed to avoid the app being crashed.

Closes: #1479

<!-- Expected Commit Message Description (imported automatically by GitHub) -->
<!-- Must conform to [conventional commits guidelines](https://github.com/edgexfoundry/device-sdk-go/blob/main/.github/Contributing.md) -->
<!-- Expected Commit message must contain Closes/Fixes #IssueNumber statement when there is a related issue -->

<!-- Add additional detailed description of need for change if no related issue -->

**If your build fails** due to your commit message not passing the build checks, please review the guidelines here: https://github.com/edgexfoundry/device-sdk-go/blob/main/.github/Contributing.md

## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] I am not introducing a breaking change (if you are, flag in conventional commit message with `BREAKING CHANGE:` describing the break)
- [x] I am not introducing a new dependency (add notes below if you are)
- [ ] I have added unit tests for the new feature or bug fix (if not, why?)
- [x] I have fully tested (add details below) this the new feature or bug fix (if not, why?)
- [ ] I have opened a PR for the related docs change (if not, why?)
  <link to docs PR>

## Testing Instructions
<!-- How can the reviewers test your change? -->
- git clone this PR
- run `make` to compile device sdk in the project base directory
- change the directory to device simple under ./example/cmd/device-simple
- run `./device-simple` without -cc flag to see the results of better error handling messages.
- repeat as needed.
- 
## New Dependency Instructions (If applicable)
<!-- Please follow [vetting instructions](https://wiki.edgexfoundry.org/display/FA/Vetting+Process+for+3rd+Party+Dependencies) and place results here -->

jim-wang-intel: on DRAFT mode until the version of go-mod-bootstrap in go.mod includes [the issue fixed for the PR](https://github.com/edgexfoundry/go-mod-bootstrap/pull/566)

Patch:
diff --git a/example/cmd/device-simple/res/configuration.yaml b/example/cmd/device-simple/res/configuration.yaml
index ef4d3a88..3d429779 100644
--- a/example/cmd/device-simple/res/configuration.yaml
+++ b/example/cmd/device-simple/res/configuration.yaml
@@ -19,8 +19,8 @@ MessageBus:
 Device:
   AsyncBufferSize: 1
   # These have common values (currently), but must be here for service local env overrides to apply when customized
-  ProfilesDir: "./res/profiles"
-  DevicesDir: "./res/devices"
+  ProfilesDir: ./res/profiles
+  DevicesDir: ./res/devices
   # Only needed if device service implements auto provisioning
   ProvisionWatchersDir: ./res/provisionwatchers
 # Example structured custom configuration
diff --git a/go.mod b/go.mod
index c067f79b..dabaefff 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.20
 
 require (
 	github.com/OneOfOne/xxhash v1.2.8
-	github.com/edgexfoundry/go-mod-bootstrap/v3 v3.1.0-dev.8
+	github.com/edgexfoundry/go-mod-bootstrap/v3 v3.1.0-dev.9
 	github.com/edgexfoundry/go-mod-core-contracts/v3 v3.1.0-dev.2
 	github.com/edgexfoundry/go-mod-messaging/v3 v3.1.0-dev.11
 	github.com/google/uuid v1.3.0
diff --git a/go.sum b/go.sum
index b3fafe53..0e77ecaa 100644
--- a/go.sum
+++ b/go.sum
@@ -28,8 +28,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik=
 github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE=
-github.com/edgexfoundry/go-mod-bootstrap/v3 v3.1.0-dev.8 h1:qoVKEMsiJM4nLJtH+0Cia6O9A/8A0w2m0KgCZDaZUNo=
-github.com/edgexfoundry/go-mod-bootstrap/v3 v3.1.0-dev.8/go.mod h1:zVtOPTNUn7jbEchgGFfbjTlLCv/KzOwqTWXxSFGIsVQ=
+github.com/edgexfoundry/go-mod-bootstrap/v3 v3.1.0-dev.9 h1:sO18almJrbT8H2BB14D6gpZe4U6i7zPH0X9xupAOo8o=
+github.com/edgexfoundry/go-mod-bootstrap/v3 v3.1.0-dev.9/go.mod h1:zVtOPTNUn7jbEchgGFfbjTlLCv/KzOwqTWXxSFGIsVQ=
 github.com/edgexfoundry/go-mod-configuration/v3 v3.1.0-dev.3 h1:xWyraOW+RtFwIO+DnCWBKoaa5w1ZX8vRf91zBa2Ll8c=
 github.com/edgexfoundry/go-mod-configuration/v3 v3.1.0-dev.3/go.mod h1:yaxBuJh45+VxXX+nSt/Q+1gGEk4/BDe9edYWm8aEtfg=
 github.com/edgexfoundry/go-mod-core-contracts/v3 v3.1.0-dev.2 h1:H3ls1vyxCv6pigZ/RZlRhj9lUTQq7CiT5/dnZRsgVmQ=
diff --git a/internal/provision/common.go b/internal/provision/common.go
new file mode 100644
index 00000000..476736ff
--- /dev/null
+++ b/internal/provision/common.go
@@ -0,0 +1,50 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//
+// # Copyright (C) 2023 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+package provision
+
+import (
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/utils"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
+	"net/url"
+	"path"
+	"strings"
+)
+
+type FileType int
+
+const (
+	YAML FileType = iota
+	JSON
+	OTHER
+)
+
+func GetFileType(fullPath string) FileType {
+	if strings.HasSuffix(fullPath, yamlExt) || strings.HasSuffix(fullPath, ymlExt) {
+		return YAML
+	} else if strings.HasSuffix(fullPath, ".json") {
+		return JSON
+	} else {
+		return OTHER
+	}
+}
+
+func GetFullAndRedactedURI(baseURI *url.URL, file, description string, lc logger.LoggingClient) (string, string) {
+	basePath, _ := path.Split(baseURI.Path)
+	newPath, err := url.JoinPath(basePath, file)
+	if err != nil {
+		lc.Error("could not join URI path for %s %s/%s: %v", description, basePath, file, err)
+		return "", ""
+	}
+	var fullURI url.URL
+	err = utils.DeepCopy(baseURI, &fullURI)
+	if err != nil {
+		lc.Error("could not copy URI for %s %s: %v", description, newPath, err)
+		return "", ""
+	}
+	fullURI.User = baseURI.User
+	fullURI.Path = newPath
+	return fullURI.String(), fullURI.Redacted()
+}
diff --git a/internal/provision/devices.go b/internal/provision/devices.go
index 64bb3f33..2a8214aa 100644
--- a/internal/provision/devices.go
+++ b/internal/provision/devices.go
@@ -2,6 +2,7 @@
 //
 // Copyright (C) 2017-2018 Canonical Ltd
 // Copyright (C) 2018-2023 IOTech Ltd
+// Copyright (C) 2023 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -11,13 +12,11 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"net/http"
-	"os"
-	"path/filepath"
-	"strings"
-
 	bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/file"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/interfaces"
 	"github.com/edgexfoundry/go-mod-bootstrap/v3/di"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
@@ -26,79 +25,38 @@ import (
 	"github.com/google/uuid"
 	"github.com/hashicorp/go-multierror"
 	"gopkg.in/yaml.v3"
+	"net/http"
+	"net/url"
+	"os"
+	"path/filepath"
 
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/container"
 )
 
 func LoadDevices(path string, dic *di.Container) errors.EdgeX {
+	var addDevicesReq []requests.AddDeviceRequest
+	var edgexErr errors.EdgeX
 	if path == "" {
 		return nil
 	}
 
-	absPath, err := filepath.Abs(path)
-	if err != nil {
-		return errors.NewCommonEdgeX(errors.KindServerError, "failed to create absolute path", err)
-	}
-
-	files, err := os.ReadDir(absPath)
-	if err != nil {
-		return errors.NewCommonEdgeX(errors.KindServerError, "failed to read directory", err)
-	}
-
-	if len(files) == 0 {
-		return nil
-	}
-
 	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
-	lc.Infof("Loading pre-defined devices from %s(%d files found)", absPath, len(files))
-
-	var addDevicesReq []requests.AddDeviceRequest
 	serviceName := container.DeviceServiceFrom(dic.Get).Name
-	for _, file := range files {
-		var devices []dtos.Device
-		fullPath := filepath.Join(absPath, file.Name())
-		if strings.HasSuffix(fullPath, yamlExt) || strings.HasSuffix(fullPath, ymlExt) {
-			content, err := os.ReadFile(fullPath)
-			if err != nil {
-				lc.Errorf("Failed to read %s: %v", fullPath, err)
-				continue
-			}
-			d := struct {
-				DeviceList []dtos.Device `yaml:"deviceList"`
-			}{}
-			err = yaml.Unmarshal(content, &d)
-			if err != nil {
-				lc.Errorf("Failed to YAML decode %s: %v", fullPath, err)
-				continue
-			}
-			devices = d.DeviceList
-		} else if strings.HasSuffix(fullPath, ".json") {
-			content, err := os.ReadFile(fullPath)
-			if err != nil {
-				lc.Errorf("Failed to read %s: %v", fullPath, err)
-				continue
-			}
-			err = json.Unmarshal(content, &devices)
-			if err != nil {
-				lc.Errorf("Failed to JSON decode %s: %v", fullPath, err)
-				continue
-			}
-		} else {
-			continue
+	parsedUrl, err := url.Parse(path)
+	if err != nil {
+		return errors.NewCommonEdgeX(errors.KindServerError, "failed to parse Devices path as a URI", err)
+	}
+	if parsedUrl.Scheme == "http" || parsedUrl.Scheme == "https" {
+		secretProvider := bootstrapContainer.SecretProviderFrom(dic.Get)
+		addDevicesReq, edgexErr = loadDevicesFromURI(path, parsedUrl, serviceName, secretProvider, lc)
+		if edgexErr != nil {
+			return edgexErr
 		}
-
-		for _, device := range devices {
-			if _, ok := cache.Devices().ForName(device.Name); ok {
-				lc.Infof("Device %s exists, using the existing one", device.Name)
-			} else {
-				lc.Infof("Device %s not found in Metadata, adding it ...", device.Name)
-				device.ServiceName = serviceName
-				device.AdminState = models.Unlocked
-				device.OperatingState = models.Up
-				req := requests.NewAddDeviceRequest(device)
-				addDevicesReq = append(addDevicesReq, req)
-			}
+	} else {
+		addDevicesReq, edgexErr = loadDevicesFromFile(path, serviceName, lc)
+		if edgexErr != nil {
+			return edgexErr
 		}
 	}
 
@@ -116,11 +74,11 @@ func LoadDevices(path string, dic *di.Container) errors.EdgeX {
 	for _, response := range responses {
 		if response.StatusCode != http.StatusCreated {
 			if response.StatusCode == http.StatusConflict {
-				lc.Warnf("%s. Device may be owned by other device service instance.", response.Message)
+				lc.Warnf("%s. Device may be owned by other Device service instance.", response.Message)
 				continue
 			}
 
-			err = multierror.Append(err, fmt.Errorf("add device failed: %s", response.Message))
+			err = multierror.Append(err, fmt.Errorf("add Device failed: %s", response.Message))
 		}
 	}
 
@@ -130,3 +88,111 @@ func LoadDevices(path string, dic *di.Container) errors.EdgeX {
 
 	return nil
 }
+
+func loadDevicesFromFile(path, serviceName string, lc logger.LoggingClient) ([]requests.AddDeviceRequest, errors.EdgeX) {
+	absPath, err := filepath.Abs(path)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to create absolute path for Devices", err)
+	}
+
+	files, err := os.ReadDir(absPath)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to read directory for Devices", err)
+	}
+
+	if len(files) == 0 {
+		return nil, nil
+	}
+
+	lc.Infof("Loading pre-defined Devices from %s(%d files found)", absPath, len(files))
+	var addDevicesReq, processedDevicesReq []requests.AddDeviceRequest
+	for _, file := range files {
+		fullPath := filepath.Join(absPath, file.Name())
+		processedDevicesReq = processDevices(fullPath, fullPath, serviceName, nil, lc)
+		if len(processedDevicesReq) > 0 {
+			addDevicesReq = append(addDevicesReq, processedDevicesReq...)
+		}
+	}
+	return addDevicesReq, nil
+}
+
+func loadDevicesFromURI(inputURI string, parsedURI *url.URL, serviceName string, secretProvider interfaces.SecretProvider, lc logger.LoggingClient) ([]requests.AddDeviceRequest, errors.EdgeX) {
+	// the input URI contains the index file containing the Device list to be loaded
+	bytes, err := file.Load(inputURI, secretProvider, lc)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf("failed to load Devices list from URI %s", parsedURI.Redacted()), err)
+	}
+
+	var files []string
+	err = json.Unmarshal(bytes, &files)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "could not unmarshal Devices list contents", err)
+	}
+
+	if len(files) == 0 {
+		lc.Infof("Index file %s for Devices list is empty", parsedURI.Redacted())
+		return nil, nil
+	}
+
+	lc.Infof("Loading pre-defined devices from %s(%d files found)", parsedURI.Redacted(), len(files))
+	var addDevicesReq, processedDevicesReq []requests.AddDeviceRequest
+	for _, file := range files {
+		fullPath, redactedPath := GetFullAndRedactedURI(parsedURI, file, "Device", lc)
+		processedDevicesReq = processDevices(fullPath, redactedPath, serviceName, secretProvider, lc)
+		if len(processedDevicesReq) > 0 {
+			addDevicesReq = append(addDevicesReq, processedDevicesReq...)
+		}
+	}
+	return addDevicesReq, nil
+}
+
+func processDevices(fullPath, displayPath, serviceName string, secretProvider interfaces.SecretProvider, lc logger.LoggingClient) []requests.AddDeviceRequest {
+	var devices []dtos.Device
+	var addDevicesReq []requests.AddDeviceRequest
+
+	fileType := GetFileType(fullPath)
+
+	// if the file type is not yaml or json, it cannot be parsed - just return to not break the loop for other devices
+	if fileType == OTHER {
+		return nil
+	}
+
+	content, err := file.Load(fullPath, secretProvider, lc)
+	if err != nil {
+		lc.Errorf("Failed to read Devices from %s: %v", displayPath, err)
+		return nil
+	}
+
+	switch fileType {
+	case YAML:
+		d := struct {
+			DeviceList []dtos.Device `yaml:"deviceList"`
+		}{}
+		err = yaml.Unmarshal(content, &d)
+		if err != nil {
+			lc.Errorf("Failed to YAML decode Devices from %s: %v", displayPath, err)
+			return nil
+		}
+		devices = d.DeviceList
+	case JSON:
+		err = json.Unmarshal(content, &devices)
+		if err != nil {
+			lc.Errorf("Failed to JSON decode Devices from %s: %v", displayPath, err)
+			return nil
+		}
+	}
+
+	for _, device := range devices {
+		if _, ok := cache.Devices().ForName(device.Name); ok {
+			lc.Infof("Device %s exists, using the existing one", device.Name)
+		} else {
+			lc.Infof("Device %s not found in Metadata, adding it ...", device.Name)
+			device.ServiceName = serviceName
+			device.AdminState = models.Unlocked
+			device.OperatingState = models.Up
+			req := requests.NewAddDeviceRequest(device)
+			addDevicesReq = append(addDevicesReq, req)
+		}
+	}
+	return addDevicesReq
+}
diff --git a/internal/provision/profiles.go b/internal/provision/profiles.go
index 99e40e87..723e9c9f 100644
--- a/internal/provision/profiles.go
+++ b/internal/provision/profiles.go
@@ -2,6 +2,7 @@
 //
 // Copyright (C) 2017-2018 Canonical Ltd
 // Copyright (C) 2018-2023 IOTech Ltd
+// Copyright (C) 2023 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -11,20 +12,22 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"os"
-	"path/filepath"
-	"strings"
-
+	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
 	bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/file"
+	bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/interfaces"
 	"github.com/edgexfoundry/go-mod-bootstrap/v3/di"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/interfaces"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
 	"github.com/google/uuid"
 	"gopkg.in/yaml.v3"
-
-	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
+	"net/url"
+	"os"
+	"path/filepath"
 )
 
 const (
@@ -34,81 +37,149 @@ const (
 )
 
 func LoadProfiles(path string, dic *di.Container) errors.EdgeX {
+	var addProfilesReq []requests.DeviceProfileRequest
+	var edgexErr errors.EdgeX
 	if path == "" {
 		return nil
 	}
+	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
+	dpc := bootstrapContainer.DeviceProfileClientFrom(dic.Get)
+	parsedUrl, err := url.Parse(path)
+	if err != nil {
+		return errors.NewCommonEdgeX(errors.KindServerError, "failed to parse Device Profile path as a URI", err)
+	}
 
+	if parsedUrl.Scheme == "http" || parsedUrl.Scheme == "https" {
+		secretProvider := bootstrapContainer.SecretProviderFrom(dic.Get)
+		addProfilesReq, edgexErr = loadProfilesFromURI(path, parsedUrl, dpc, secretProvider, lc)
+		if edgexErr != nil {
+			return edgexErr
+		}
+	} else {
+		addProfilesReq, edgexErr = loadProfilesFromFile(path, dpc, lc)
+		if edgexErr != nil {
+			return edgexErr
+		}
+	}
+
+	if len(addProfilesReq) == 0 {
+		return nil
+	}
+	ctx := context.WithValue(context.Background(), common.CorrelationHeader, uuid.NewString()) // nolint:staticcheck
+	_, edgexErr = dpc.Add(ctx, addProfilesReq)
+	return edgexErr
+}
+
+func loadProfilesFromFile(path string, dpc interfaces.DeviceProfileClient, lc logger.LoggingClient) ([]requests.DeviceProfileRequest, errors.EdgeX) {
+	var edgexErr errors.EdgeX
 	absPath, err := filepath.Abs(path)
 	if err != nil {
-		return errors.NewCommonEdgeX(errors.KindServerError, "failed to create absolute path", err)
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to create absolute path for profiles", err)
 	}
 
 	files, err := os.ReadDir(absPath)
 	if err != nil {
-		return errors.NewCommonEdgeX(errors.KindServerError, "failed to read directory", err)
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to read directory for profiles", err)
 	}
 
 	if len(files) == 0 {
-		return nil
+		return nil, nil
 	}
 
-	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
-	lc.Infof("Loading pre-defined profiles from %s(%d files found)", absPath, len(files))
-
-	var addProfilesReq []requests.DeviceProfileRequest
-	dpc := bootstrapContainer.DeviceProfileClientFrom(dic.Get)
+	lc.Infof("Loading pre-defined Device Profiles from %s(%d files found)", absPath, len(files))
+	var addProfilesReq, processedProfilesReq []requests.DeviceProfileRequest
 	for _, file := range files {
-		var profile dtos.DeviceProfile
 		fullPath := filepath.Join(absPath, file.Name())
-		if strings.HasSuffix(fullPath, yamlExt) || strings.HasSuffix(fullPath, ymlExt) {
-			content, err := os.ReadFile(fullPath)
-			if err != nil {
-				lc.Errorf("Failed to read %s: %v", fullPath, err)
-				continue
-			}
+		processedProfilesReq, edgexErr = processProfiles(fullPath, fullPath, nil, lc, dpc)
+		if edgexErr != nil {
+			return nil, edgexErr
+		}
+		if len(processedProfilesReq) > 0 {
+			addProfilesReq = append(addProfilesReq, processedProfilesReq...)
+		}
+	}
+	return addProfilesReq, nil
+}
 
-			err = yaml.Unmarshal(content, &profile)
-			if err != nil {
-				lc.Errorf("Failed to YAML decode profile %s: %v", file.Name(), err)
-				continue
-			}
-		} else if strings.HasSuffix(fullPath, jsonExt) {
-			content, err := os.ReadFile(fullPath)
-			if err != nil {
-				lc.Errorf("Failed to read %s: %v", fullPath, err)
-				continue
-			}
+func loadProfilesFromURI(inputURI string, parsedURI *url.URL, dpc interfaces.DeviceProfileClient, secretProvider bootstrapInterfaces.SecretProvider, lc logger.LoggingClient) ([]requests.DeviceProfileRequest, errors.EdgeX) {
+	var edgexErr errors.EdgeX
+	// the input URI contains the index file containing the Profile list to be loaded
+	bytes, err := file.Load(inputURI, secretProvider, lc)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf("failed to load Device Profile list from URI %s", parsedURI.Redacted()), err)
+	}
 
-			err = json.Unmarshal(content, &profile)
-			if err != nil {
-				lc.Errorf("Failed to JSON decode profile %s: %v", file.Name(), err)
-				continue
-			}
-		} else {
-			continue
+	var files []string
+	err = json.Unmarshal(bytes, &files)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "could not unmarshal Profile list contents", err)
+	}
+	if len(files) == 0 {
+		lc.Infof("Index file %s for Device Profiles list is empty", parsedURI.Redacted())
+		return nil, nil
+	}
+
+	lc.Infof("Loading pre-defined Device Profiles from %s(%d files found)", parsedURI.Redacted(), len(files))
+	var addProfilesReq, processedProfilesReq []requests.DeviceProfileRequest
+	for _, file := range files {
+		fullPath, redactedPath := GetFullAndRedactedURI(parsedURI, file, "Device Profile", lc)
+		processedProfilesReq, edgexErr = processProfiles(fullPath, redactedPath, secretProvider, lc, dpc)
+		if edgexErr != nil {
+			return nil, edgexErr
+		}
+		if len(processedProfilesReq) > 0 {
+			addProfilesReq = append(addProfilesReq, processedProfilesReq...)
 		}
+	}
+	return addProfilesReq, nil
+}
 
-		res, err := dpc.DeviceProfileByName(context.Background(), profile.Name)
-		if err == nil {
-			lc.Infof("Profile %s exists, using the existing one", profile.Name)
-			_, exist := cache.Profiles().ForName(profile.Name)
-			if !exist {
-				err = cache.Profiles().Add(dtos.ToDeviceProfileModel(res.Profile))
-				if err != nil {
-					return errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf("failed to cache the profile %s", res.Profile.Name), err)
-				}
-			}
-		} else {
-			lc.Infof("Profile %s not found in Metadata, adding it ...", profile.Name)
-			req := requests.NewDeviceProfileRequest(profile)
-			addProfilesReq = append(addProfilesReq, req)
+func processProfiles(fullPath, displayPath string, secretProvider bootstrapInterfaces.SecretProvider, lc logger.LoggingClient, dpc interfaces.DeviceProfileClient) ([]requests.DeviceProfileRequest, errors.EdgeX) {
+	var profile dtos.DeviceProfile
+	var addProfilesReq []requests.DeviceProfileRequest
+
+	fileType := GetFileType(fullPath)
+
+	// if the file type is not yaml or json, it cannot be parsed - just return to not break the loop for other devices
+	if fileType == OTHER {
+		return nil, nil
+	}
+
+	content, err := file.Load(fullPath, secretProvider, lc)
+	if err != nil {
+		lc.Errorf("Failed to read Device Profile from %s: %v", displayPath, err)
+		return nil, nil
+	}
+
+	switch fileType {
+	case YAML:
+		err = yaml.Unmarshal(content, &profile)
+		if err != nil {
+			lc.Errorf("Failed to YAML decode Device Profile from %s: %v", displayPath, err)
+			return nil, nil
+		}
+	case JSON:
+		err = json.Unmarshal(content, &profile)
+		if err != nil {
+			lc.Errorf("Failed to JSON decode Device Profile from %s: %v", displayPath, err)
+			return nil, nil
 		}
 	}
 
-	if len(addProfilesReq) == 0 {
-		return nil
+	res, err := dpc.DeviceProfileByName(context.Background(), profile.Name)
+	if err == nil {
+		lc.Infof("Device Profile %s exists, using the existing one", profile.Name)
+		_, exist := cache.Profiles().ForName(profile.Name)
+		if !exist {
+			err = cache.Profiles().Add(dtos.ToDeviceProfileModel(res.Profile))
+			if err != nil {
+				return addProfilesReq, errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf("failed to cache the profile %s", res.Profile.Name), err)
+			}
+		}
+	} else {
+		lc.Infof("Device Profile %s not found in Metadata, adding it ...", profile.Name)
+		req := requests.NewDeviceProfileRequest(profile)
+		addProfilesReq = append(addProfilesReq, req)
 	}
-	ctx := context.WithValue(context.Background(), common.CorrelationHeader, uuid.NewString()) // nolint:staticcheck
-	_, edgexErr := dpc.Add(ctx, addProfilesReq)
-	return edgexErr
+	return addProfilesReq, nil
 }
diff --git a/internal/provision/provisionwatchers.go b/internal/provision/provisionwatchers.go
index 873fc8b8..60238d30 100644
--- a/internal/provision/provisionwatchers.go
+++ b/internal/provision/provisionwatchers.go
@@ -1,5 +1,6 @@
 //
 // Copyright (C) 2023 IOTech Ltd
+// Copyright (C) 2023 Intel Corporation
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -8,91 +9,160 @@ package provision
 import (
 	"context"
 	"encoding/json"
-	"os"
-	"path/filepath"
-	"strings"
-
+	"fmt"
+	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
 	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/file"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/interfaces"
 	"github.com/edgexfoundry/go-mod-bootstrap/v3/di"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/common"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
 	"github.com/google/uuid"
 	"gopkg.in/yaml.v3"
-
-	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
+	"net/url"
+	"os"
+	"path/filepath"
 )
 
 func LoadProvisionWatchers(path string, dic *di.Container) errors.EdgeX {
+	var addProvisionWatchersReq []requests.AddProvisionWatcherRequest
+	var edgexErr errors.EdgeX
 	if path == "" {
 		return nil
 	}
 
-	absPath, err := filepath.Abs(path)
+	lc := container.LoggingClientFrom(dic.Get)
+
+	parsedUrl, err := url.Parse(path)
 	if err != nil {
-		return errors.NewCommonEdgeX(errors.KindServerError, "failed to create absolute path", err)
+		return errors.NewCommonEdgeX(errors.KindServerError, "failed to parse Provision Watcher path as a URI", err)
+	}
+	if parsedUrl.Scheme == "http" || parsedUrl.Scheme == "https" {
+		secretProvider := container.SecretProviderFrom(dic.Get)
+		addProvisionWatchersReq, edgexErr = loadProvisionWatchersFromURI(path, parsedUrl, secretProvider, lc)
+		if edgexErr != nil {
+			return edgexErr
+		}
+	} else {
+		addProvisionWatchersReq, edgexErr = loadProvisionWatchersFromFile(path, lc)
+		if edgexErr != nil {
+			return edgexErr
+		}
+	}
+	if len(addProvisionWatchersReq) == 0 {
+		return nil
 	}
 
-	files, err := os.ReadDir(absPath)
+	pwc := container.ProvisionWatcherClientFrom(dic.Get)
+	ctx := context.WithValue(context.Background(), common.CorrelationHeader, uuid.NewString()) //nolint: staticcheck
+	_, edgexErr = pwc.Add(ctx, addProvisionWatchersReq)
+	return edgexErr
+}
+
+func loadProvisionWatchersFromFile(path string, lc logger.LoggingClient) ([]requests.AddProvisionWatcherRequest, errors.EdgeX) {
+	absPath, err := filepath.Abs(path)
 	if err != nil {
-		return errors.NewCommonEdgeX(errors.KindServerError, "failed to read directory", err)
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to create absolute path for Provision Watchers", err)
+	}
+	files, err := os.ReadDir(path)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "failed to read directory for Provision Watchers", err)
 	}
 
 	if len(files) == 0 {
-		return nil
+		return nil, nil
 	}
 
-	lc := container.LoggingClientFrom(dic.Get)
-	lc.Infof("Loading pre-defined provision watchers from %s(%d files found)", absPath, len(files))
-
-	var addProvisionWatchersReq []requests.AddProvisionWatcherRequest
+	lc.Infof("Loading pre-defined Provision Watchers from %s(%d files found)", absPath, len(files))
+	var addProvisionWatchersReq, processedProvisionWatchersReq []requests.AddProvisionWatcherRequest
 	for _, file := range files {
-		var watcher dtos.ProvisionWatcher
-		filename := filepath.Join(absPath, file.Name())
-		data, err := os.ReadFile(filename)
-		if err != nil {
-			lc.Errorf("Failed to read %s: %v", filename, err)
-			continue
+		fullPath := filepath.Join(absPath, file.Name())
+		processedProvisionWatchersReq = processProvisionWatcherFile(fullPath, fullPath, nil, lc)
+		if len(processedProvisionWatchersReq) > 0 {
+			addProvisionWatchersReq = append(addProvisionWatchersReq, processedProvisionWatchersReq...)
 		}
+	}
+	return addProvisionWatchersReq, nil
+}
+
+func loadProvisionWatchersFromURI(inputURI string, parsedURI *url.URL, secretProvider interfaces.SecretProvider, lc logger.LoggingClient) ([]requests.AddProvisionWatcherRequest, errors.EdgeX) {
+	// the input URI contains the index file containing the Provision Watcher list to be loaded
+	bytes, err := file.Load(inputURI, secretProvider, lc)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, fmt.Sprintf("failed to load Provision Watchers list from URI %s", parsedURI.Redacted()), err)
+	}
+
+	var files []string
+
+	err = json.Unmarshal(bytes, &files)
+	if err != nil {
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, "could not unmarshal Provision Watcher list contents", err)
+	}
 
-		if strings.HasSuffix(filename, yamlExt) || strings.HasSuffix(filename, ymlExt) {
-			err = yaml.Unmarshal(data, &watcher)
-			if err != nil {
-				lc.Errorf("Failed to YAML decode %s: %v", filename, err)
-				continue
-			}
-		} else if strings.HasSuffix(filename, jsonExt) {
-			err := json.Unmarshal(data, &watcher)
-			if err != nil {
-				lc.Errorf("Failed to JSON decode %s: %v", filename, err)
-				continue
-			}
-		} else {
-			continue
+	if len(files) == 0 {
+		lc.Infof("Index file %s for Provision Watchers list is empty", parsedURI.Redacted())
+		return nil, nil
+	}
+
+	lc.Infof("Loading pre-defined Provision Watchers from %s(%d files found)", parsedURI.Redacted(), len(files))
+	var addProvisionWatchersReq, processedProvisionWatchersReq []requests.AddProvisionWatcherRequest
+	for _, file := range files {
+		fullPath, redactedPath := GetFullAndRedactedURI(parsedURI, file, "Provison Watcher", lc)
+		processedProvisionWatchersReq = processProvisionWatcherFile(fullPath, redactedPath, secretProvider, lc)
+		if len(processedProvisionWatchersReq) > 0 {
+			addProvisionWatchersReq = append(addProvisionWatchersReq, processedProvisionWatchersReq...)
 		}
+	}
+	return addProvisionWatchersReq, nil
+}
+
+func processProvisionWatcherFile(fullPath, displayPath string, secretProvider interfaces.SecretProvider, lc logger.LoggingClient) []requests.AddProvisionWatcherRequest {
+	var watcher dtos.ProvisionWatcher
+	var addProvisionWatchersReq []requests.AddProvisionWatcherRequest
 
-		err = common.Validate(watcher)
+	fileType := GetFileType(fullPath)
+
+	// if the file type is not yaml or json, it cannot be parsed - just return to not break the loop for other devices
+	if fileType == OTHER {
+		return nil
+	}
+
+	content, err := file.Load(fullPath, secretProvider, lc)
+	if err != nil {
+		lc.Errorf("Failed to read Provision Watcher from %s: %v", displayPath, err)
+		return nil
+	}
+
+	switch fileType {
+	case YAML:
+		err = yaml.Unmarshal(content, &watcher)
 		if err != nil {
-			lc.Errorf("ProvisionWatcher %s validation failed: %v", watcher.Name, err)
-			continue
+			lc.Errorf("Failed to YAML decode Provision Watcher from %s: %v", displayPath, err)
+			return nil
 		}
-
-		if _, ok := cache.ProvisionWatchers().ForName(watcher.Name); ok {
-			lc.Infof("ProvisionWatcher %s exists, using the existing one", watcher.Name)
-		} else {
-			lc.Infof("ProvisionWatcher %s not found in Metadata, adding it...", watcher.Name)
-			req := requests.NewAddProvisionWatcherRequest(watcher)
-			addProvisionWatchersReq = append(addProvisionWatchersReq, req)
+	case JSON:
+		err = json.Unmarshal(content, &watcher)
+		if err != nil {
+			lc.Errorf("Failed to JSON decode Provision Watcher from %s: %v", displayPath, err)
+			return nil
 		}
 	}
 
-	if len(addProvisionWatchersReq) == 0 {
+	err = common.Validate(watcher)
+	if err != nil {
+		lc.Errorf("ProvisionWatcher %s validation failed: %v", watcher.Name, err)
 		return nil
 	}
 
-	pwc := container.ProvisionWatcherClientFrom(dic.Get)
-	ctx := context.WithValue(context.Background(), common.CorrelationHeader, uuid.NewString()) //nolint: staticcheck
-	_, edgexErr := pwc.Add(ctx, addProvisionWatchersReq)
-	return edgexErr
+	if _, ok := cache.ProvisionWatchers().ForName(watcher.Name); ok {
+		lc.Infof("ProvisionWatcher %s exists, using the existing one", watcher.Name)
+	} else {
+		lc.Infof("ProvisionWatcher %s not found in Metadata, adding it...", watcher.Name)
+		req := requests.NewAddProvisionWatcherRequest(watcher)
+		addProvisionWatchersReq = append(addProvisionWatchersReq, req)
+	}
+	return addProvisionWatchersReq
 }
diff --git a/pkg/service/init.go b/pkg/service/init.go
index 18a6f6a6..cd89955a 100644
--- a/pkg/service/init.go
+++ b/pkg/service/init.go
@@ -51,6 +51,11 @@ func (b *Bootstrap) BootstrapHandler(ctx context.Context, wg *sync.WaitGroup, _
 	s.controller = http.NewRestController(b.router, dic, s.serviceKey)
 	s.controller.InitRestRoutes()
 
+	if bootstrapContainer.DeviceClientFrom(dic.Get) == nil {
+		s.lc.Error("Client configuration for core-metadata not found, missing common config? Use -cp or -cc flags for common config.")
+		return false
+	}
+
 	edgexErr := cache.InitCache(s.serviceKey, s.baseServiceName, dic)
 	if edgexErr != nil {
 		s.lc.Errorf("Failed to init cache: %s", edgexErr.Error())
diff --git a/pkg/service/service.go b/pkg/service/service.go
index 514786f7..54adbd5b 100644
--- a/pkg/service/service.go
+++ b/pkg/service/service.go
@@ -188,6 +188,9 @@ func (s *deviceService) MetricsManager() bootstrapInterfaces.MetricsManager {
 
 // LoggingClient returns the logger.LoggingClient
 func (s *deviceService) LoggingClient() logger.LoggingClient {
+	if s.lc == nil {
+		s.lc = bootstrapContainer.LoggingClientFrom(s.dic.Get)
+	}
 	return s.lc
 }
 


Test patch:
diff --git a/internal/provision/common_test.go b/internal/provision/common_test.go
new file mode 100644
index 00000000..cd2a4f39
--- /dev/null
+++ b/internal/provision/common_test.go
@@ -0,0 +1,57 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//
+// # Copyright (C) 2023 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+package provision
+
+import (
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"net/url"
+	"path"
+	"testing"
+)
+
+func Test_GetFileType(t *testing.T) {
+	tests := []struct {
+		name             string
+		path             string
+		expectedFileType FileType
+	}{
+		{"valid get Yaml file type", path.Join("..", "..", "example", "cmd", "device-simple", "res", "devices", "simple-device.yml"), YAML},
+		{"valid get Json file type", path.Join("..", "..", "example", "cmd", "device-simple", "res", "devices", "simple-device.json"), JSON},
+		{"valid get other file type", path.Join("..", "..", "example", "cmd", "device-simple", "res", "devices", "simple-device.bogus"), OTHER},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			actualType := GetFileType(tt.path)
+			assert.Equal(t, tt.expectedFileType, actualType)
+		})
+	}
+}
+
+func Test_GetFullAndRedactedURI(t *testing.T) {
+	tests := []struct {
+		name             string
+		baseURI          string
+		file             string
+		expectedURI      string
+		expectedRedacted string
+	}{
+		{"valid no secret uri", "https://raw.githubusercontent.com/edgexfoundry/device-virtual-go/main/cmd/res/devices/devices.yaml", "device-simple.yaml", "https://raw.githubusercontent.com/edgexfoundry/device-virtual-go/main/cmd/res/devices/device-simple.yaml", "https://raw.githubusercontent.com/edgexfoundry/device-virtual-go/main/cmd/res/devices/device-simple.yaml"},
+		{"valid query secret uri", "https://raw.githubusercontent.com/edgexfoundry/device-simple/main/devices/index.json?edgexSecretName=githubCredentials", "device-simple.yaml", "https://raw.githubusercontent.com/edgexfoundry/device-simple/main/devices/device-simple.yaml?edgexSecretName=githubCredentials", "https://raw.githubusercontent.com/edgexfoundry/device-simple/main/devices/device-simple.yaml?edgexSecretName=githubCredentials"},
+		{"valid query secret uri", "https://myuser:mypassword@raw.githubusercontent.com/edgexfoundry/device-simple/main/devices/index.json", "device-simple.yaml", "https://myuser:mypassword@raw.githubusercontent.com/edgexfoundry/device-simple/main/devices/device-simple.yaml", "https://myuser:xxxxx@raw.githubusercontent.com/edgexfoundry/device-simple/main/devices/device-simple.yaml"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			testURI, err := url.Parse(tt.baseURI)
+			require.NoError(t, err)
+			lc := logger.MockLogger{}
+			actualURI, actualRedacted := GetFullAndRedactedURI(testURI, tt.file, "test", lc)
+			assert.Equal(t, tt.expectedURI, actualURI)
+			assert.Equal(t, tt.expectedRedacted, actualRedacted)
+		})
+	}
+}
diff --git a/internal/provision/devices_test.go b/internal/provision/devices_test.go
new file mode 100644
index 00000000..17eecfcf
--- /dev/null
+++ b/internal/provision/devices_test.go
@@ -0,0 +1,81 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//
+// # Copyright (C) 2023 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+package provision
+
+import (
+	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/interfaces"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"net/url"
+	"path"
+	"testing"
+)
+
+func Test_processDevices(t *testing.T) {
+	tests := []struct {
+		name               string
+		path               string
+		secretProvider     interfaces.SecretProvider
+		expectedNumDevices int
+	}{
+		{"valid load device from file", path.Join("..", "..", "example", "cmd", "device-simple", "res", "devices", "simple-device.yml"), nil, 1},
+		{"valid load devices from uri", "https://raw.githubusercontent.com/edgexfoundry/device-virtual-go/main/cmd/res/devices/devices.yaml", nil, 5},
+		{"invalid load device empty path", "", nil, 0},
+		{"invalid load device from file", path.Join("..", "..", "example", "cmd", "device-simple", "res", "devices", "bogus.yml"), nil, 0},
+		{"invalid load device invalid uri", "https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/example/cmd/device-simple/res/devices/bogus.yml", nil, 0},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			lc := logger.MockLogger{}
+			dic, _ := NewMockDIC()
+			err := cache.InitCache(TestDeviceService, TestDeviceService, dic)
+			require.NoError(t, err)
+			addDeviceRequests := processDevices(tt.path, tt.path, TestDeviceService, tt.secretProvider, lc)
+			assert.Equal(t, tt.expectedNumDevices, len(addDeviceRequests))
+		})
+	}
+}
+
+func Test_loadDevicesFromURI(t *testing.T) {
+	tests := []struct {
+		name                string
+		path                string
+		serviceName         string
+		secretProvider      interfaces.SecretProvider
+		expectedNumDevices  int
+		expectedEdgexErrMsg string
+	}{
+		{"valid load from uri",
+			"https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/internal/provision/uri-test-files/devices/index.json",
+			"TestDevice",
+			nil,
+			2, ""},
+		{"invalid load from uri",
+			"https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/internal/provision/uri-test-files/devices/bogus.json",
+			"TestDevice",
+			nil,
+			0, "failed to load Devices list from URI"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var addDeviceReq []requests.AddDeviceRequest
+			lc := logger.MockLogger{}
+			dic, _ := NewMockDIC()
+			edgexErr := cache.InitCache(TestDeviceService, TestDeviceService, dic)
+			require.NoError(t, edgexErr)
+			parsedURI, err := url.Parse(tt.path)
+			require.NoError(t, err)
+			addDeviceReq, edgexErr = loadDevicesFromURI(tt.path, parsedURI, tt.serviceName, tt.secretProvider, lc)
+			assert.Equal(t, tt.expectedNumDevices, len(addDeviceReq))
+			if edgexErr != nil {
+				assert.Contains(t, edgexErr.Error(), tt.expectedEdgexErrMsg)
+			}
+		})
+	}
+}
diff --git a/internal/provision/mockdic_test.go b/internal/provision/mockdic_test.go
new file mode 100644
index 00000000..52854191
--- /dev/null
+++ b/internal/provision/mockdic_test.go
@@ -0,0 +1,119 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//
+// # Copyright (C) 2023 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+package provision
+
+import (
+	"context"
+	"github.com/edgexfoundry/device-sdk-go/v3/internal/config"
+	"github.com/edgexfoundry/device-sdk-go/v3/internal/container"
+	bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/di"
+	clientMocks "github.com/edgexfoundry/go-mod-core-contracts/v3/clients/interfaces/mocks"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/models"
+)
+
+const (
+	TestDeviceService             = "testDeviceService"
+	TestDeviceWithTags            = "testDeviceWithTags"
+	TestDeviceWithoutTags         = "testDeviceWithoutTags"
+	TestProfile                   = "testProfile"
+	TestDeviceResourceWithTags    = "testResourceWithTags"
+	TestDeviceResourceWithoutTags = "testResourceWithoutTags"
+	TestDeviceCommandWithTags     = "testCommandWithTags"
+	TestDeviceCommandWithoutTags  = "testCommandWithoutTags"
+	TestResourceTagName           = "testResourceTagName"
+	TestResourceTagValue          = "testResourceTagValue"
+	TestCommandTagName            = "testCommandTagName"
+	TestCommandTagValue           = "testCommandTagValue"
+	TestDeviceTagName             = "testDeviceTagName"
+	TestDeviceTagValue            = "testDeviceTagValue"
+	TestDuplicateTagName          = "testDuplicateTagName"
+)
+
+var profile = responses.DeviceProfileResponse{
+	Profile: dtos.DeviceProfile{
+		DeviceProfileBasicInfo: dtos.DeviceProfileBasicInfo{Name: TestProfile},
+		DeviceResources: []dtos.DeviceResource{
+			{
+				Name: TestDeviceResourceWithTags,
+				Tags: dtos.Tags{
+					TestResourceTagName: TestResourceTagValue,
+				},
+			},
+			{
+				Name: TestDeviceResourceWithoutTags,
+			},
+		},
+		DeviceCommands: []dtos.DeviceCommand{
+			{
+				Name: TestDeviceCommandWithTags,
+				Tags: dtos.Tags{
+					TestCommandTagName:   TestCommandTagValue,
+					TestDuplicateTagName: TestCommandTagValue,
+				},
+			},
+			{
+				Name: TestDeviceCommandWithoutTags,
+			},
+		},
+	},
+}
+
+func NewMockDIC() (*di.Container, *clientMocks.DeviceProfileClient) {
+	configuration := &config.ConfigurationStruct{
+		Device: config.DeviceInfo{MaxCmdOps: 1},
+	}
+	deviceService := &models.DeviceService{Name: TestDeviceService}
+
+	devices := responses.MultiDevicesResponse{
+		Devices: []dtos.Device{
+			{
+				Name:        TestDeviceWithTags,
+				ProfileName: TestProfile,
+				Tags: dtos.Tags{
+					TestDeviceTagName:    TestDeviceTagValue,
+					TestDuplicateTagName: TestDeviceTagValue,
+				},
+			},
+			{
+				Name:        TestDeviceWithoutTags,
+				ProfileName: TestProfile,
+			},
+		},
+	}
+	dcMock := &clientMocks.DeviceClient{}
+	dcMock.On("DevicesByServiceName", context.Background(), TestDeviceService, 0, -1).Return(devices, nil)
+
+	dpcMock := &clientMocks.DeviceProfileClient{}
+	dpcMock.On("DeviceProfileByName", context.Background(), TestProfile).Return(profile, nil)
+
+	pwcMock := &clientMocks.ProvisionWatcherClient{}
+	pwcMock.On("ProvisionWatchersByServiceName", context.Background(), TestDeviceService, 0, -1).Return(responses.MultiProvisionWatchersResponse{}, nil)
+
+	return di.NewContainer(di.ServiceConstructorMap{
+		container.ConfigurationName: func(get di.Get) interface{} {
+			return configuration
+		},
+		container.DeviceServiceName: func(get di.Get) any {
+			return deviceService
+		},
+		bootstrapContainer.LoggingClientInterfaceName: func(get di.Get) interface{} {
+			return logger.NewMockClient()
+		},
+		bootstrapContainer.DeviceClientName: func(get di.Get) interface{} {
+			return dcMock
+		},
+		bootstrapContainer.DeviceProfileClientName: func(get di.Get) interface{} {
+			return dpcMock
+		},
+		bootstrapContainer.ProvisionWatcherClientName: func(get di.Get) interface{} {
+			return pwcMock
+		},
+	}), dpcMock
+}
diff --git a/internal/provision/profiles_test.go b/internal/provision/profiles_test.go
new file mode 100644
index 00000000..a20dc22e
--- /dev/null
+++ b/internal/provision/profiles_test.go
@@ -0,0 +1,257 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//
+// # Copyright (C) 2023 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+package provision
+
+import (
+	"context"
+	goErrors "errors"
+	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/interfaces"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"net/url"
+	"path"
+	"testing"
+)
+
+var simpleProfile = responses.DeviceProfileResponse{
+	Profile: dtos.DeviceProfile{
+		DeviceProfileBasicInfo: dtos.DeviceProfileBasicInfo{
+			Name:         "Simple-Device",
+			Manufacturer: "Simple Corp.",
+			Description:  "Example of Simple Device",
+			Model:        "SP-01",
+			Labels:       []string{"modbus"},
+		},
+		DeviceResources: []dtos.DeviceResource{
+			{
+				Description: "Switch On/Off.",
+				Name:        "SwitchButton",
+				IsHidden:    false,
+				Properties: dtos.ResourceProperties{
+					ValueType:    "Bool",
+					ReadWrite:    "RW",
+					DefaultValue: "true",
+				},
+			},
+			{
+				Description: "Visual representation of Switch state.",
+				Name:        "Image",
+				IsHidden:    false,
+				Properties: dtos.ResourceProperties{
+					ValueType: "Binary",
+					ReadWrite: "R",
+					MediaType: "image/jpeg",
+				},
+			},
+			{
+				Description: "X axis rotation rate",
+				Name:        "Xrotation",
+				IsHidden:    true,
+				Properties: dtos.ResourceProperties{
+					ValueType: "Int32",
+					ReadWrite: "RW",
+					Units:     "rpm",
+				},
+			},
+			{
+				Description: "y axis rotation rate",
+				Name:        "yrotation",
+				IsHidden:    true,
+				Properties: dtos.ResourceProperties{
+					ValueType: "Int32",
+					ReadWrite: "RW",
+					Units:     "rpm",
+				},
+			},
+			{
+				Description: "z axis rotation rate",
+				Name:        "zrotation",
+				IsHidden:    true,
+				Properties: dtos.ResourceProperties{
+					ValueType: "Int32",
+					ReadWrite: "RW",
+					Units:     "rpm",
+				},
+			},
+			{
+				Description: "String array",
+				Name:        "StringArray",
+				IsHidden:    false,
+				Properties: dtos.ResourceProperties{
+					ValueType: "StringArray",
+					ReadWrite: "RW",
+				},
+			},
+			{
+				Description: "Unsigned 8bit array",
+				Name:        "Uint8Array",
+				IsHidden:    false,
+				Properties: dtos.ResourceProperties{
+					ValueType: "Uint8Array",
+					ReadWrite: "RW",
+				},
+			},
+			{
+				Description: "Counter data",
+				Name:        "Counter",
+				IsHidden:    false,
+				Properties: dtos.ResourceProperties{
+					ValueType: "Object",
+					ReadWrite: "RW",
+				},
+			},
+		},
+		DeviceCommands: []dtos.DeviceCommand{
+			{
+				Name:      "Switch",
+				IsHidden:  false,
+				ReadWrite: "RW",
+				ResourceOperations: []dtos.ResourceOperation{
+					{
+						DeviceResource: "SwitchButton",
+						DefaultValue:   "false",
+					},
+				},
+			},
+			{
+				Name:      "Image",
+				IsHidden:  false,
+				ReadWrite: "R",
+				ResourceOperations: []dtos.ResourceOperation{
+					{
+						DeviceResource: "Image",
+					},
+				},
+			},
+			{
+				Name:      "Rotation",
+				IsHidden:  false,
+				ReadWrite: "RW",
+				ResourceOperations: []dtos.ResourceOperation{
+					{
+						DeviceResource: "XRotation",
+						DefaultValue:   "0",
+					},
+					{
+						DeviceResource: "YRotation",
+						DefaultValue:   "0",
+					},
+					{
+						DeviceResource: "ZRotation",
+						DefaultValue:   "0",
+					},
+				},
+			},
+		},
+	},
+}
+
+func Test_processProfiles(t *testing.T) {
+	tests := []struct {
+		name                string
+		path                string
+		secretProvider      interfaces.SecretProvider
+		profileName         string
+		dpcMockRes          responses.DeviceProfileResponse
+		dpcMockErr          errors.EdgeX
+		expectedNumProfiles int
+		expectedEdgexErrMsg string
+	}{
+		{"valid load profile from file, profile exists", path.Join("..", "..", "example", "cmd", "device-simple", "res", "profiles", "Simple-Driver.yaml"), nil, "Simple-Device", simpleProfile, nil, 0, ""},
+		{"valid load profile from file, profile does not exist in metadata", path.Join("..", "..", "example", "cmd", "device-simple", "res", "profiles", "Simple-Driver.yaml"), nil, "Simple-Device", responses.DeviceProfileResponse{}, errors.NewCommonEdgeXWrapper(goErrors.New("could not find profile")), 1, ""},
+		{"valid load profile from uri, profile exists", "https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/example/cmd/device-simple/res/profiles/Simple-Driver.yaml", nil, "Simple-Device", simpleProfile, nil, 0, ""},
+		{"valid load profile from uri, profile does not exist in metadata", "https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/example/cmd/device-simple/res/profiles/Simple-Driver.yaml", nil, "Simple-Device", responses.DeviceProfileResponse{}, errors.NewCommonEdgeXWrapper(goErrors.New("could not find profile")), 1, ""},
+		{"invalid load empty profile from file", "", nil, "Simple-Device", responses.DeviceProfileResponse{}, errors.NewCommonEdgeXWrapper(goErrors.New("could not find profile")), 0, ""},
+		{"invalid load profile from bogus file", path.Join("..", "..", "example", "cmd", "device-simple", "res", "profiles", "bogus.yaml"), nil, "Simple-Device", responses.DeviceProfileResponse{}, nil, 0, ""},
+		{"invalid load profile from bogus uri", "https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/example/cmd/device-simple/res/profiles/bogus.yaml", nil, "Simple-Device", responses.DeviceProfileResponse{}, nil, 0, ""},
+		{"invalid load profile from file, duplicate profile", path.Join("..", "..", "example", "cmd", "device-simple", "res", "profiles", "Simple-Driver.yaml"), nil, "Simple-Device", profile, nil, 0, "Profile testProfile has already existed in cache"},
+		{"invalid load profile from uri, duplicate profile", "https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/example/cmd/device-simple/res/profiles/Simple-Driver.yaml", nil, "Simple-Device", profile, nil, 0, "Profile testProfile has already existed in cache"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var addProfilesReq []requests.DeviceProfileRequest
+			var edgexErr errors.EdgeX
+			lc := logger.MockLogger{}
+			dic, dpcMock := NewMockDIC()
+			dpcMock.On("DeviceProfileByName", context.Background(), tt.profileName).Return(tt.dpcMockRes, tt.dpcMockErr)
+			err := cache.InitCache(TestDeviceService, TestDeviceService, dic)
+			require.NoError(t, err)
+			addProfilesReq, edgexErr = processProfiles(tt.path, tt.path, tt.secretProvider, lc, dpcMock)
+			assert.Equal(t, len(addProfilesReq), tt.expectedNumProfiles)
+			if edgexErr != nil {
+				assert.Contains(t, edgexErr.Error(), tt.expectedEdgexErrMsg)
+			}
+		})
+	}
+}
+
+func Test_loadProfilesFromURI(t *testing.T) {
+	tests := []struct {
+		name                string
+		path                string
+		secretProvider      interfaces.SecretProvider
+		profileNames        []string
+		dpcMockRes          []responses.DeviceProfileResponse
+		dpcMockErr          []errors.EdgeX
+		expectedNumProfiles int
+		expectedEdgexErrMsg string
+	}{
+		{"valid load from uri, profile exists",
+			"https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/internal/provision/uri-test-files/profiles/index.json",
+			nil,
+			[]string{"Simple-Device", "Simple-Device2"},
+			[]responses.DeviceProfileResponse{simpleProfile, simpleProfile},
+			[]errors.EdgeX{nil, nil},
+			0, ""},
+		{"valid load from uri, profile does not exist in metadata",
+			"https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/internal/provision/uri-test-files/profiles/index.json",
+			nil,
+			[]string{"Simple-Device", "Simple-Device2"},
+			[]responses.DeviceProfileResponse{responses.DeviceProfileResponse{}, responses.DeviceProfileResponse{}},
+			[]errors.EdgeX{errors.NewCommonEdgeXWrapper(goErrors.New("could not find profile")), errors.NewCommonEdgeXWrapper(goErrors.New("could not find profile"))},
+			2, ""},
+		{"valid load where one profile exists and one profile does not exist in metadata",
+			"https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/internal/provision/uri-test-files/profiles/index.json",
+			nil,
+			[]string{"Simple-Device", "Simple-Device2"},
+			[]responses.DeviceProfileResponse{simpleProfile, responses.DeviceProfileResponse{}},
+			[]errors.EdgeX{nil, errors.NewCommonEdgeXWrapper(goErrors.New("could not find profile"))},
+			1, ""},
+		{"invalid load profile path join breaks",
+			"https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/internal/provision/uri-test-files/profiles/bogus.json",
+			nil,
+			[]string{},
+			[]responses.DeviceProfileResponse{},
+			[]errors.EdgeX{},
+			0, "failed to load Device Profile list from URI"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var addProfilesReq []requests.DeviceProfileRequest
+			var edgexErr errors.EdgeX
+			lc := logger.MockLogger{}
+			dic, dpcMock := NewMockDIC()
+			for index := range tt.profileNames {
+				dpcMock.On("DeviceProfileByName", context.Background(), tt.profileNames[index]).Return(tt.dpcMockRes[index], tt.dpcMockErr[index])
+			}
+			edgexErr = cache.InitCache(TestDeviceService, TestDeviceService, dic)
+			require.NoError(t, edgexErr)
+			parsedURI, err := url.Parse(tt.path)
+			require.NoError(t, err)
+			addProfilesReq, edgexErr = loadProfilesFromURI(tt.path, parsedURI, dpcMock, tt.secretProvider, lc)
+			assert.Equal(t, len(addProfilesReq), tt.expectedNumProfiles)
+			if edgexErr != nil {
+				assert.Contains(t, edgexErr.Error(), tt.expectedEdgexErrMsg)
+			}
+		})
+	}
+}
diff --git a/internal/provision/provisionwatchers_test.go b/internal/provision/provisionwatchers_test.go
new file mode 100644
index 00000000..98a8c3de
--- /dev/null
+++ b/internal/provision/provisionwatchers_test.go
@@ -0,0 +1,79 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//
+// # Copyright (C) 2023 Intel Corporation
+//
+// SPDX-License-Identifier: Apache-2.0
+package provision
+
+import (
+	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
+	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/interfaces"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/clients/logger"
+	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"net/url"
+	"path"
+	"testing"
+)
+
+func Test_processProvisionWatcherFile(t *testing.T) {
+	tests := []struct {
+		name                         string
+		path                         string
+		secretProvider               interfaces.SecretProvider
+		expectedNumProvisionWatchers int
+	}{
+		{"valid load provision watcher from file", path.Join("..", "..", "example", "cmd", "device-simple", "res", "provisionwatchers", "Simple-Provision-Watcher.yml"), nil, 1},
+		{"valid load provision watcher from valid uri", "https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/example/cmd/device-simple/res/provisionwatchers/Simple-Provision-Watcher.yml", nil, 1},
+		{"invalid load provision watcher empty path", "", nil, 0},
+		{"invalid load provision watcher from file", path.Join("..", "..", "example", "cmd", "device-simple", "res", "provisionwatchers", "bogus.yml"), nil, 0},
+		{"invalid load provision watcher from invalid uri", "https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/example/cmd/device-simple/res/provisionwatchers/bogus.yml", nil, 0},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var addProvisionWatchersReq []requests.AddProvisionWatcherRequest
+			lc := logger.MockLogger{}
+			dic, _ := NewMockDIC()
+			err := cache.InitCache(TestDeviceService, TestDeviceService, dic)
+			require.NoError(t, err)
+			addProvisionWatchersReq = processProvisionWatcherFile(tt.path, tt.path, tt.secretProvider, lc)
+			assert.Equal(t, tt.expectedNumProvisionWatchers, len(addProvisionWatchersReq))
+		})
+	}
+}
+
+func Test_loadProvisionWatchersFromURI(t *testing.T) {
+	tests := []struct {
+		name                         string
+		path                         string
+		secretProvider               interfaces.SecretProvider
+		expectedNumProvisionWatchers int
+		expectedEdgexErrMsg          string
+	}{
+		{"valid load from uri",
+			"https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/internal/provision/uri-test-files/provisionwatchers/index.json",
+			nil,
+			2, ""},
+		{"invalid load from uri",
+			"https://raw.githubusercontent.com/edgexfoundry/device-sdk-go/main/internal/provision/uri-test-files/provisionwatchers/bogus.json",
+			nil,
+			0, "failed to load Provision Watchers list from URI"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var addProvisionWatchersReq []requests.AddProvisionWatcherRequest
+			lc := logger.MockLogger{}
+			dic, _ := NewMockDIC()
+			edgexErr := cache.InitCache(TestDeviceService, TestDeviceService, dic)
+			require.NoError(t, edgexErr)
+			parsedURI, err := url.Parse(tt.path)
+			require.NoError(t, err)
+			addProvisionWatchersReq, edgexErr = loadProvisionWatchersFromURI(tt.path, parsedURI, tt.secretProvider, lc)
+			assert.Equal(t, tt.expectedNumProvisionWatchers, len(addProvisionWatchersReq))
+			if edgexErr != nil {
+				assert.Contains(t, edgexErr.Error(), tt.expectedEdgexErrMsg)
+			}
+		})
+	}
+}
diff --git a/internal/provision/uri-test-files/README.md b/internal/provision/uri-test-files/README.md
new file mode 100644
index 00000000..db69c3b8
--- /dev/null
+++ b/internal/provision/uri-test-files/README.md
@@ -0,0 +1,4 @@
+# Purpose
+
+These files are used to test the URI for files feature for devices, profiles, and provision watchers. 
+The files will be loaded off of the main branch of device-sdk-go using the GitHub URI. 
\ No newline at end of file
diff --git a/internal/provision/uri-test-files/devices/index.json b/internal/provision/uri-test-files/devices/index.json
new file mode 100644
index 00000000..9bcb746f
--- /dev/null
+++ b/internal/provision/uri-test-files/devices/index.json
@@ -0,0 +1,3 @@
+[
+  "simple-device.yml","simple-device2.yaml"
+]
\ No newline at end of file
diff --git a/internal/provision/uri-test-files/devices/simple-device.yml b/internal/provision/uri-test-files/devices/simple-device.yml
new file mode 100644
index 00000000..75ba733e
--- /dev/null
+++ b/internal/provision/uri-test-files/devices/simple-device.yml
@@ -0,0 +1,17 @@
+deviceList:
+  - name: Simple-Device01
+    profileName: Simple-Device
+    description: Example of Simple Device
+    labels:
+      - industrial
+    protocols:
+      other:
+        Address: simple01
+        Port: 300
+    autoEvents:
+      - interval: 10s
+        onChange: false
+        sourceName: Switch
+      - interval: 30s
+        onChange: false
+        sourceName: Image
diff --git a/internal/provision/uri-test-files/devices/simple-device2.yaml b/internal/provision/uri-test-files/devices/simple-device2.yaml
new file mode 100644
index 00000000..451cd8ca
--- /dev/null
+++ b/internal/provision/uri-test-files/devices/simple-device2.yaml
@@ -0,0 +1,17 @@
+deviceList:
+  - name: Simple-Device02
+    profileName: Simple-Device2
+    description: Example of Simple Device
+    labels:
+      - industrial
+    protocols:
+      other:
+        Address: simple02
+        Port: 300
+    autoEvents:
+      - interval: 10s
+        onChange: false
+        sourceName: Switch
+      - interval: 30s
+        onChange: false
+        sourceName: Image
diff --git a/internal/provision/uri-test-files/profiles/Simple-Driver.yaml b/internal/provision/uri-test-files/profiles/Simple-Driver.yaml
new file mode 100644
index 00000000..5b67b9d3
--- /dev/null
+++ b/internal/provision/uri-test-files/profiles/Simple-Driver.yaml
@@ -0,0 +1,92 @@
+apiVersion: "v2"
+name: "Simple-Device"
+manufacturer: "Simple Corp."
+model: "SP-01"
+labels:
+  - "modbus"
+description: "Example of Simple Device"
+
+deviceResources:
+  -
+    name: "SwitchButton"
+    isHidden: false
+    description: "Switch On/Off."
+    properties:
+        valueType: "Bool"
+        readWrite: "RW"
+        defaultValue: "true"
+  -
+    name: "Image"
+    isHidden: false
+    description: "Visual representation of Switch state."
+    properties:
+        valueType: "Binary"
+        readWrite: "R"
+        mediaType: "image/jpeg"
+  -
+    name: "Xrotation"
+    isHidden: true
+    description: "X axis rotation rate"
+    properties:
+        valueType: "Int32"
+        readWrite: "RW"
+        units: "rpm"
+  -
+    name: "Yrotation"
+    isHidden: true
+    description: "Y axis rotation rate"
+    properties:
+        valueType: "Int32"
+        readWrite: "RW"
+        "units": "rpm"
+  -
+    name: "Zrotation"
+    isHidden: true
+    description: "Z axis rotation rate"
+    properties:
+        valueType: "Int32"
+        readWrite: "RW"
+        "units": "rpm"
+  -
+    name: "StringArray"
+    isHidden: false
+    description: "String array"
+    properties:
+      valueType: "StringArray"
+      readWrite: "RW"
+  -
+    name: "Uint8Array"
+    isHidden: false
+    description: "Unsigned 8bit array"
+    properties:
+        valueType: "Uint8Array"
+        readWrite: "RW"
+  -
+    name: "Counter"
+    isHidden: false
+    description: "Counter data"
+    properties:
+      valueType: "Object"
+      readWrite: "RW"
+
+deviceCommands:
+  -
+    name: "Switch"
+    isHidden: false
+    readWrite: "RW"
+    resourceOperations:
+      - { deviceResource: "SwitchButton", defaultValue: "false" }
+  -
+    name: "Image"
+    isHidden: false
+    readWrite: "R"
+    resourceOperations:
+      - { deviceResource: "Image" }
+  -
+    name: "Rotation"
+    isHidden: false
+    readWrite: "RW"
+    resourceOperations:
+      - { deviceResource: "Xrotation", defaultValue: "0" }
+      - { deviceResource: "Yrotation", defaultValue: "0" }
+      - { deviceResource: "Zrotation", defaultValue: "0" }
diff --git a/internal/provision/uri-test-files/profiles/Simple-Driver2.yml b/internal/provision/uri-test-files/profiles/Simple-Driver2.yml
new file mode 100644
index 00000000..8766fc95
--- /dev/null
+++ b/internal/provision/uri-test-files/profiles/Simple-Driver2.yml
@@ -0,0 +1,92 @@
+apiVersion: "v2"
+name: "Simple-Device2"
+manufacturer: "Simple Corp."
+model: "SP-01"
+labels:
+  - "modbus"
+description: "Example of Simple Device"
+
+deviceResources:
+  -
+    name: "SwitchButton"
+    isHidden: false
+    description: "Switch On/Off."
+    properties:
+        valueType: "Bool"
+        readWrite: "RW"
+        defaultValue: "true"
+  -
+    name: "Image"
+    isHidden: false
+    description: "Visual representation of Switch state."
+    properties:
+        valueType: "Binary"
+        readWrite: "R"
+        mediaType: "image/jpeg"
+  -
+    name: "Xrotation"
+    isHidden: true
+    description: "X axis rotation rate"
+    properties:
+        valueType: "Int32"
+        readWrite: "RW"
+        units: "rpm"
+  -
+    name: "Yrotation"
+    isHidden: true
+    description: "Y axis rotation rate"
+    properties:
+        valueType: "Int32"
+        readWrite: "RW"
+        "units": "rpm"
+  -
+    name: "Zrotation"
+    isHidden: true
+    description: "Z axis rotation rate"
+    properties:
+        valueType: "Int32"
+        readWrite: "RW"
+        "units": "rpm"
+  -
+    name: "StringArray"
+    isHidden: false
+    description: "String array"
+    properties:
+      valueType: "StringArray"
+      readWrite: "RW"
+  -
+    name: "Uint8Array"
+    isHidden: false
+    description: "Unsigned 8bit array"
+    properties:
+        valueType: "Uint8Array"
+        readWrite: "RW"
+  -
+    name: "Counter"
+    isHidden: false
+    description: "Counter data"
+    properties:
+      valueType: "Object"
+      readWrite: "RW"
+
+deviceCommands:
+  -
+    name: "Switch"
+    isHidden: false
+    readWrite: "RW"
+    resourceOperations:
+      - { deviceResource: "SwitchButton", defaultValue: "false" }
+  -
+    name: "Image"
+    isHidden: false
+    readWrite: "R"
+    resourceOperations:
+      - { deviceResource: "Image" }
+  -
+    name: "Rotation"
+    isHidden: false
+    readWrite: "RW"
+    resourceOperations:
+      - { deviceResource: "Xrotation", defaultValue: "0" }
+      - { deviceResource: "Yrotation", defaultValue: "0" }
+      - { deviceResource: "Zrotation", defaultValue: "0" }
diff --git a/internal/provision/uri-test-files/profiles/index.json b/internal/provision/uri-test-files/profiles/index.json
new file mode 100644
index 00000000..8b21dfd9
--- /dev/null
+++ b/internal/provision/uri-test-files/profiles/index.json
@@ -0,0 +1,3 @@
+[
+  "Simple-Driver.yaml","Simple-Driver2.yml"
+]
\ No newline at end of file
diff --git a/internal/provision/uri-test-files/provisionwatchers/Simple-Provision-Watcher.yml b/internal/provision/uri-test-files/provisionwatchers/Simple-Provision-Watcher.yml
new file mode 100644
index 00000000..a48b8dbc
--- /dev/null
+++ b/internal/provision/uri-test-files/provisionwatchers/Simple-Provision-Watcher.yml
@@ -0,0 +1,22 @@
+name: Simple-Provision-Watcher
+serviceName: device-simple
+labels:
+  - simple
+identifiers:
+  Address: simple[0-9]+
+  Port: 3[0-9]{2}
+blockingIdentifiers:
+  Port:
+    - 397
+    - 398
+    - 399
+adminState: UNLOCKED
+discoveredDevice:
+  profileName: Simple-Device
+  adminState: UNLOCKED
+  autoEvents:
+    - interval: 15s
+      sourceName: SwitchButton
+  properties:
+    testPropertyA: weather
+    testPropertyB: meter
diff --git a/internal/provision/uri-test-files/provisionwatchers/Simple-Provision-Watcher2.yaml b/internal/provision/uri-test-files/provisionwatchers/Simple-Provision-Watcher2.yaml
new file mode 100644
index 00000000..71149cf1
--- /dev/null
+++ b/internal/provision/uri-test-files/provisionwatchers/Simple-Provision-Watcher2.yaml
@@ -0,0 +1,22 @@
+name: Simple-Provision-Watcher2
+serviceName: device-simple
+labels:
+  - simple
+identifiers:
+  Address: simple[0-9]+
+  Port: 3[0-9]{2}
+blockingIdentifiers:
+  Port:
+    - 397
+    - 398
+    - 399
+adminState: UNLOCKED
+discoveredDevice:
+  profileName: Simple-Device2
+  adminState: UNLOCKED
+  autoEvents:
+    - interval: 15s
+      sourceName: SwitchButton
+  properties:
+    testPropertyA: weather
+    testPropertyB: meter
diff --git a/internal/provision/uri-test-files/provisionwatchers/index.json b/internal/provision/uri-test-files/provisionwatchers/index.json
new file mode 100644
index 00000000..26c83d28
--- /dev/null
+++ b/internal/provision/uri-test-files/provisionwatchers/index.json
@@ -0,0 +1,3 @@
+[
+  "Simple-Provision-Watcher.yml","Simple-Provision-Watcher2.yaml"
+]
\ No newline at end of file