Repository: edgexfoundry/device-sdk-go (go)

Issue:
Device service PUT Resource handling shall normally publish an update to the Message Bus
# 🚀 Feature Request

### Relevant Package [**REQUIRED**]
This feature request is a change for the device-sdk-go handling of the PUT /device/name/{device}/{command} API.
Later will extend to the device-sdk-c.

### Description [**REQUIRED**]
Core-command's GET /device/name/{device}/{command} has an optional query parameter, ds-pushevent, which is described as "If set to yes, a successful GET will result in an event being pushed to the EdgeX system". 
There is a desire for a similar capability for the matching PUT API, to notify other services of an update to the Resource(s), and to update core-data.


### Describe the solution you'd like
Change the Device SDK's PUT /device/name/{device}/{command} API handling, as normally sent by core-command,
to publish the updated Resource value(s) to the Message Bus as long as the Resource(s) are not write-only.

### Describe alternatives you've considered
The workaround today is to follow the PUT with a GET /device/name/{device}/{command} with ds-pushevent set to true, but this is just extra traffic to achieve the desired end.

The first draft of this Feature Request described adding a field to the data payload to indicate that the Resource value(s) should be published to the Message Bus. But, in discussion at the Architect's meeting, it was decided that we would almost always want to have the value published. The exception is for secrets or other write-only resources. So no need to require an additional field, and no worry that it might be omitted when it should be provided.


Pull request description:
feat: publish event with updated value for PUT command
Similiar to GET command with 'ds-pushevent=true', publish an event with updated resource(s) value to messageBus as long as the resource/command is not write-only.

closes #1271

<!-- 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)
- [x] 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?)
- [x] I have opened a PR for the related docs change (if not, why?)
  https://github.com/edgexfoundry/edgex-docs/pull/983

## Testing Instructions
<!-- How can the reviewers test your change? -->
1. Run `device-simple` from this branch
2. Subscribe to edgex internal event topics in `redis` container:
```shell
$ docker exec -it edgex-redis /bin/sh
/data # redis-cli
127.0.0.1:6379> PSUBSCRIBE edgex.events.device.*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "edgex.events.device.*"
3) (integer) 1
1) "pmessage"
2) "edgex.events.device.*"
3) "edgex.events.device.device-simple.Simple-Device.Simple-Device01.Xrotation"
4) "{\"ReceivedTopic\":\"\",\"CorrelationID\":\"24683755-b598-4dda-8f3e-29536ef2b3d6\",\"ApiVersion\":\"v2\",\"RequestID\":\"\",\"ErrorCode\":0,\"Payload\":\"eyJhcGlWZXJzaW9uIjoidjIiLCJyZXF1ZXN0SWQiOiJkODZjOTliOS1iMGE0LTQ2MzgtODgxOS03ODAxYTJjZTA4ZWUiLCJldmVudCI6eyJhcGlWZXJzaW9uIjoidjIiLCJpZCI6ImM1ZDllN2E4LWE3NzEtNGNkNS04NTBmLTU4OTJhMDIxNDE1NiIsImRldmljZU5hbWUiOiJTaW1wbGUtRGV2aWNlMDEiLCJwcm9maWxlTmFtZSI6IlNpbXBsZS1EZXZpY2UiLCJzb3VyY2VOYW1lIjoiWHJvdGF0aW9uIiwib3JpZ2luIjoxNjc3ODM1Nzc1NTcyNzMwMDAwLCJyZWFkaW5ncyI6W3siaWQiOiIxMTRlZGNmYS1hNTAwLTRmMGEtYTBkMC02MTNlZDU5ZWYyZGQiLCJvcmlnaW4iOjE2Nzc4MzU3NzU1NzI3MzAwMDAsImRldmljZU5hbWUiOiJTaW1wbGUtRGV2aWNlMDEiLCJyZXNvdXJjZU5hbWUiOiJYcm90YXRpb24iLCJwcm9maWxlTmFtZSI6IlNpbXBsZS1EZXZpY2UiLCJ2YWx1ZVR5cGUiOiJJbnQzMiIsInVuaXRzIjoicnBtIiwidmFsdWUiOiIxMCJ9XX19\",\"ContentType\":\"application/json\",\"QueryParams\":{}}"
```
3. Verify there's an event being published after a successfully PUT command:
```shell
$ curl -X PUT -H "Content-Type: application/json" -d '{"Xrotation": "10"}' http://localhost:59882/api/v2/device/name/Simple-Device01/Xrotation
{"apiVersion":"v2","statusCode":200}
```

base64 decode the payload:
```json
{
   "apiVersion":"v2",
   "requestId":"d86c99b9-b0a4-4638-8819-7801a2ce08ee",
   "event":{
      "apiVersion":"v2",
      "id":"c5d9e7a8-a771-4cd5-850f-5892a0214156",
      "deviceName":"Simple-Device01",
      "profileName":"Simple-Device",
      "sourceName":"Xrotation",
      "origin":1677835775572730000,
      "readings":[
         {
            "id":"114edcfa-a500-4f0a-a0d0-613ed59ef2dd",
            "origin":1677835775572730000,
            "deviceName":"Simple-Device01",
            "resourceName":"Xrotation",
            "profileName":"Simple-Device",
            "valueType":"Int32",
            "units":"rpm",
            "value":"10"
         }
      ]
   }
}
```

Note that this behavior also applies to commands via messaging.


## 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 -->

Patch:
diff --git a/go.mod b/go.mod
index adcd90d8..13f0ff86 100644
--- a/go.mod
+++ b/go.mod
@@ -4,9 +4,10 @@ go 1.18
 
 require (
 	github.com/OneOfOne/xxhash v1.2.8
-	github.com/edgexfoundry/go-mod-bootstrap/v3 v3.0.0-dev.44
+	github.com/edgexfoundry/go-mod-bootstrap/v3 v3.0.0-dev.42
 	github.com/edgexfoundry/go-mod-core-contracts/v3 v3.0.0-dev.26
 	github.com/edgexfoundry/go-mod-messaging/v3 v3.0.0-dev.16
+	github.com/edgexfoundry/go-mod-registry/v3 v3.0.0-dev.5
 	github.com/google/uuid v1.3.0
 	github.com/gorilla/mux v1.8.0
 	github.com/pelletier/go-toml v1.9.5
@@ -22,7 +23,6 @@ require (
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/eclipse/paho.mqtt.golang v1.4.2 // indirect
 	github.com/edgexfoundry/go-mod-configuration/v3 v3.0.0-dev.7 // indirect
-	github.com/edgexfoundry/go-mod-registry/v3 v3.0.0-dev.5 // indirect
 	github.com/edgexfoundry/go-mod-secrets/v3 v3.0.0-dev.8 // indirect
 	github.com/fatih/color v1.9.0 // indirect
 	github.com/fxamacker/cbor/v2 v2.4.0 // indirect
diff --git a/go.sum b/go.sum
index b0326b39..cad27206 100644
--- a/go.sum
+++ b/go.sum
@@ -27,8 +27,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4=
 github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA=
-github.com/edgexfoundry/go-mod-bootstrap/v3 v3.0.0-dev.44 h1:bzROMV3XZzFB5uBFTeQRuqBQyHE8DJjHSl3UWoqacds=
-github.com/edgexfoundry/go-mod-bootstrap/v3 v3.0.0-dev.44/go.mod h1:qiG4HABtB+mnFBOby/ZUpIWcQD0TZa8jIG7p/jeVuzc=
+github.com/edgexfoundry/go-mod-bootstrap/v3 v3.0.0-dev.42 h1:7EqTqlEIsubvn5yIZP0Yk7edt6xlXbzGqpVTeygcGRE=
+github.com/edgexfoundry/go-mod-bootstrap/v3 v3.0.0-dev.42/go.mod h1:qiG4HABtB+mnFBOby/ZUpIWcQD0TZa8jIG7p/jeVuzc=
 github.com/edgexfoundry/go-mod-configuration/v3 v3.0.0-dev.7 h1:pOH2GLDg1KB4EmAzo6IEvl4NEVFAw3ywxPUMa5Wi1Vw=
 github.com/edgexfoundry/go-mod-configuration/v3 v3.0.0-dev.7/go.mod h1:ZZbOu7K0/P8B1VKhZygVujLQyhvWuPe0E2vC/k2yscw=
 github.com/edgexfoundry/go-mod-core-contracts/v3 v3.0.0-dev.26 h1:vTTaaM3NErQRoNQKkbQ9f9x/FMJqzZX6OAQuXvU6HN8=
diff --git a/internal/application/command.go b/internal/application/command.go
index d6441342..193b5fa2 100644
--- a/internal/application/command.go
+++ b/internal/application/command.go
@@ -63,33 +63,34 @@ func GetCommand(ctx context.Context, deviceName string, commandName string, quer
 	return res, nil
 }
 
-func SetCommand(ctx context.Context, deviceName string, commandName string, queryParams string, requests map[string]any, dic *di.Container) errors.EdgeX {
+func SetCommand(ctx context.Context, deviceName string, commandName string, queryParams string, requests map[string]any, dic *di.Container) (*dtos.Event, errors.EdgeX) {
 	if deviceName == "" {
-		return errors.NewCommonEdgeX(errors.KindContractInvalid, "device name is empty", nil)
+		return nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "device name is empty", nil)
 	}
 	if commandName == "" {
-		return errors.NewCommonEdgeX(errors.KindContractInvalid, "command is empty", nil)
+		return nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "command is empty", nil)
 	}
 
 	device, err := validateServiceAndDeviceState(deviceName, dic)
 	if err != nil {
-		return errors.NewCommonEdgeXWrapper(err)
+		return nil, errors.NewCommonEdgeXWrapper(err)
 	}
 
+	var event *dtos.Event
 	_, cmdExist := cache.Profiles().DeviceCommand(device.ProfileName, commandName)
 	if cmdExist {
-		err = writeDeviceCommand(device, commandName, queryParams, requests, dic)
+		event, err = writeDeviceCommand(device, commandName, queryParams, requests, dic)
 	} else {
-		err = writeDeviceResource(device, commandName, queryParams, requests, dic)
+		event, err = writeDeviceResource(device, commandName, queryParams, requests, dic)
 	}
 
 	if err != nil {
-		return errors.NewCommonEdgeXWrapper(err)
+		return nil, errors.NewCommonEdgeXWrapper(err)
 	}
 
 	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
 	lc.Debugf("SET Device Command successfully. Device: %s, Source: %s, %s: %s", deviceName, commandName, common.CorrelationHeader, utils.FromContext(ctx, common.CorrelationHeader))
-	return nil
+	return event, nil
 }
 
 func readDeviceResource(device models.Device, resourceName string, attributes string, dic *di.Container) (res *dtos.Event, edgexErr errors.EdgeX) {
@@ -128,7 +129,8 @@ func readDeviceResource(device models.Device, resourceName string, attributes st
 	}
 
 	// convert CommandValue to Event
-	res, edgexErr = transformer.CommandValuesToEventDTO(results, device.Name, dr.Name, dic)
+	configuration := container.ConfigurationFrom(dic.Get)
+	res, edgexErr = transformer.CommandValuesToEventDTO(results, device.Name, dr.Name, configuration.Device.DataTransform, dic)
 	if edgexErr != nil {
 		return res, errors.NewCommonEdgeX(errors.KindServerError, "failed to convert CommandValue to Event", err)
 	}
@@ -185,7 +187,7 @@ func readDeviceCommand(device models.Device, commandName string, attributes stri
 	}
 
 	// convert CommandValue to Event
-	res, edgexErr = transformer.CommandValuesToEventDTO(results, device.Name, dc.Name, dic)
+	res, edgexErr = transformer.CommandValuesToEventDTO(results, device.Name, dc.Name, configuration.Device.DataTransform, dic)
 	if edgexErr != nil {
 		return res, errors.NewCommonEdgeX(errors.KindServerError, "failed to transform CommandValue to Event", edgexErr)
 	}
@@ -193,16 +195,16 @@ func readDeviceCommand(device models.Device, commandName string, attributes stri
 	return res, nil
 }
 
-func writeDeviceResource(device models.Device, resourceName string, attributes string, requests map[string]any, dic *di.Container) errors.EdgeX {
+func writeDeviceResource(device models.Device, resourceName string, attributes string, requests map[string]any, dic *di.Container) (*dtos.Event, errors.EdgeX) {
 	dr, ok := cache.Profiles().DeviceResource(device.ProfileName, resourceName)
 	if !ok {
 		errMsg := fmt.Sprintf("deviceResource %s not found", resourceName)
-		return errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
+		return nil, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
 	}
 	// check deviceResource is not read-only
 	if dr.Properties.ReadWrite == common.ReadWrite_R {
 		errMsg := fmt.Sprintf("deviceResource %s is marked as read-only", dr.Name)
-		return errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
+		return nil, errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
 	}
 
 	// check set parameters contains provided deviceResource
@@ -212,14 +214,14 @@ func writeDeviceResource(device models.Device, resourceName string, attributes s
 			v = dr.Properties.DefaultValue
 		} else {
 			errMsg := fmt.Sprintf("deviceResource %s not found in request body and no default value defined", dr.Name)
-			return errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
+			return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
 		}
 	}
 
 	// create CommandValue
 	cv, edgexErr := createCommandValueFromDeviceResource(dr, v)
 	if edgexErr != nil {
-		return errors.NewCommonEdgeX(errors.Kind(edgexErr), "failed to create CommandValue", edgexErr)
+		return nil, errors.NewCommonEdgeX(errors.Kind(edgexErr), "failed to create CommandValue", edgexErr)
 	}
 
 	// prepare CommandRequest
@@ -239,7 +241,7 @@ func writeDeviceResource(device models.Device, resourceName string, attributes s
 	if configuration.Device.DataTransform {
 		edgexErr = transformer.TransformWriteParameter(cv, dr.Properties)
 		if edgexErr != nil {
-			return errors.NewCommonEdgeX(errors.KindContractInvalid, "failed to transform set parameter", edgexErr)
+			return nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "failed to transform set parameter", edgexErr)
 		}
 	}
 
@@ -248,28 +250,33 @@ func writeDeviceResource(device models.Device, resourceName string, attributes s
 	err := driver.HandleWriteCommands(device.Name, device.Protocols, reqs, []*sdkModels.CommandValue{cv})
 	if err != nil {
 		errMsg := fmt.Sprintf("error writing DeviceResource %s for %s", dr.Name, device.Name)
-		return errors.NewCommonEdgeX(errors.KindServerError, errMsg, err)
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, err)
 	}
 
-	return nil
+	// Updated resource value will be published to MessageBus as long as it's not write-only
+	if dr.Properties.ReadWrite != common.ReadWrite_W {
+		return transformer.CommandValuesToEventDTO([]*sdkModels.CommandValue{cv}, device.Name, resourceName, false, dic)
+	}
+
+	return nil, nil
 }
 
-func writeDeviceCommand(device models.Device, commandName string, attributes string, requests map[string]any, dic *di.Container) errors.EdgeX {
+func writeDeviceCommand(device models.Device, commandName string, attributes string, requests map[string]any, dic *di.Container) (*dtos.Event, errors.EdgeX) {
 	dc, ok := cache.Profiles().DeviceCommand(device.ProfileName, commandName)
 	if !ok {
 		errMsg := fmt.Sprintf("deviceCommand %s not found", commandName)
-		return errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
+		return nil, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
 	}
 	// check deviceCommand is not read-only
 	if dc.ReadWrite == common.ReadWrite_R {
 		errMsg := fmt.Sprintf("deviceCommand %s is marked as read-only", dc.Name)
-		return errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
+		return nil, errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
 	}
 	// check ResourceOperation count does not exceed MaxCmdOps defined in configuration
 	configuration := container.ConfigurationFrom(dic.Get)
 	if len(dc.ResourceOperations) > configuration.Device.MaxCmdOps {
 		errMsg := fmt.Sprintf("SET command %s exceed device %s MaxCmdOps (%d)", dc.Name, device.Name, configuration.Device.MaxCmdOps)
-		return errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
 	}
 
 	// create CommandValues
@@ -280,7 +287,7 @@ func writeDeviceCommand(device models.Device, commandName string, attributes str
 		dr, ok := cache.Profiles().DeviceResource(device.ProfileName, drName)
 		if !ok {
 			errMsg := fmt.Sprintf("deviceResource %s in SET commnd %s for %s not defined", drName, dc.Name, device.Name)
-			return errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
+			return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
 		}
 
 		// check request body contains the deviceResource
@@ -292,7 +299,7 @@ func writeDeviceCommand(device models.Device, commandName string, attributes str
 				value = dr.Properties.DefaultValue
 			} else {
 				errMsg := fmt.Sprintf("deviceResource %s not found in request body and no default value defined", dr.Name)
-				return errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
+				return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
 			}
 		}
 
@@ -312,7 +319,7 @@ func writeDeviceCommand(device models.Device, commandName string, attributes str
 		if err == nil {
 			cvs = append(cvs, cv)
 		} else {
-			return errors.NewCommonEdgeX(errors.Kind(err), "failed to create CommandValue", err)
+			return nil, errors.NewCommonEdgeX(errors.Kind(err), "failed to create CommandValue", err)
 		}
 	}
 
@@ -335,7 +342,7 @@ func writeDeviceCommand(device models.Device, commandName string, attributes str
 		if configuration.Device.DataTransform {
 			err := transformer.TransformWriteParameter(cv, dr.Properties)
 			if err != nil {
-				return errors.NewCommonEdgeX(errors.KindContractInvalid, "failed to transform set parameter", err)
+				return nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "failed to transform set parameter", err)
 			}
 		}
 	}
@@ -345,10 +352,15 @@ func writeDeviceCommand(device models.Device, commandName string, attributes str
 	err := driver.HandleWriteCommands(device.Name, device.Protocols, reqs, cvs)
 	if err != nil {
 		errMsg := fmt.Sprintf("error writing DeviceCommand %s for %s", dc.Name, device.Name)
-		return errors.NewCommonEdgeX(errors.KindServerError, errMsg, err)
+		return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, err)
+	}
+
+	// Updated resource(s) value will be published to MessageBus as long as they're not write-only
+	if dc.ReadWrite != common.ReadWrite_W {
+		return transformer.CommandValuesToEventDTO(cvs, device.Name, commandName, false, dic)
 	}
 
-	return nil
+	return nil, nil
 }
 
 func validateServiceAndDeviceState(deviceName string, dic *di.Container) (models.Device, errors.EdgeX) {
diff --git a/internal/common/utils.go b/internal/common/utils.go
index 50f53b3d..35f8bcb0 100644
--- a/internal/common/utils.go
+++ b/internal/common/utils.go
@@ -9,9 +9,10 @@ package common
 
 import (
 	"context"
+	"net/url"
+
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/container"
-	"net/url"
 
 	bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
 	bootstrapInterfaces "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/interfaces"
diff --git a/internal/config/config.go b/internal/config/config.go
index eb0507bb..93b18c2f 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -46,11 +46,6 @@ func (c *ConfigurationStruct) EmptyWritablePtr() interface{} {
 	return &WritableInfo{}
 }
 
-// GetWritablePtr returns pointer to the writable section
-func (c *ConfigurationStruct) GetWritablePtr() any {
-	return &c.Writable
-}
-
 // UpdateWritableFromRaw converts configuration received from the registry to a service-specific WritableInfo struct
 // which is then used to overwrite the service's existing configuration's WritableInfo struct.
 func (c *ConfigurationStruct) UpdateWritableFromRaw(rawWritable interface{}) bool {
diff --git a/internal/controller/http/command.go b/internal/controller/http/command.go
index f218604e..2aa5d724 100644
--- a/internal/controller/http/command.go
+++ b/internal/controller/http/command.go
@@ -1,6 +1,6 @@
 // -*- Mode: Go; indent-tabs-mode: t -*-
 //
-// Copyright (C) 2020-2022 IOTech Ltd
+// Copyright (C) 2020-2023 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -83,12 +83,17 @@ func (c *RestController) SetCommand(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	err = application.SetCommand(ctx, deviceName, commandName, queryParams, requestParamsMap, c.dic)
+	event, err := application.SetCommand(ctx, deviceName, commandName, queryParams, requestParamsMap, c.dic)
 	if err != nil {
 		c.sendEdgexError(w, r, err, common.ApiDeviceNameCommandNameRoute)
 		return
 	}
 
+	if event != nil {
+		correlationId := utils.FromContext(ctx, common.CorrelationHeader)
+		go sdkCommon.SendEvent(event, correlationId, c.dic)
+	}
+
 	res := commonDTO.NewBaseResponse("", "", http.StatusOK)
 	c.sendResponse(w, r, common.ApiDeviceNameCommandNameRoute, res, http.StatusOK)
 }
diff --git a/internal/controller/messaging/command.go b/internal/controller/messaging/command.go
index 60e072c9..4336b407 100644
--- a/internal/controller/messaging/command.go
+++ b/internal/controller/messaging/command.go
@@ -1,5 +1,5 @@
 //
-// Copyright (C) 2022 IOTech Ltd
+// Copyright (C) 2022-2023 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -183,7 +183,7 @@ func setCommand(ctx context.Context, msgEnvelope types.MessageEnvelope, response
 
 	// TODO: fix properly in EdgeX 3.0
 	ctx = context.WithValue(ctx, common.CorrelationHeader, msgEnvelope.CorrelationID) // nolint: staticcheck
-	edgexErr := application.SetCommand(ctx, deviceName, commandName, rawQuery, requestPayload, dic)
+	event, edgexErr := application.SetCommand(ctx, deviceName, commandName, rawQuery, requestPayload, dic)
 	if edgexErr != nil {
 		lc.Errorf("Failed to process set device command %s for device %s: %s", commandName, deviceName, edgexErr.Error())
 		responseEnvelope = types.NewMessageEnvelopeWithError(msgEnvelope.RequestID, edgexErr.Error())
@@ -210,6 +210,10 @@ func setCommand(ctx context.Context, msgEnvelope types.MessageEnvelope, response
 		lc.Errorf("Failed to publish command response: %s", err.Error())
 		return
 	}
+
+	if event != nil {
+		go sdkCommon.SendEvent(event, msgEnvelope.CorrelationID, dic)
+	}
 }
 
 func filterQueryParams(queries map[string]string) (string, bool, bool) {
diff --git a/internal/transformer/transform.go b/internal/transformer/transform.go
index e1cec2fa..e6ad9a0c 100644
--- a/internal/transformer/transform.go
+++ b/internal/transformer/transform.go
@@ -27,7 +27,7 @@ var (
 	originMutex    sync.Mutex
 )
 
-func CommandValuesToEventDTO(cvs []*models.CommandValue, deviceName string, sourceName string, dic *di.Container) (*dtos.Event, errors.EdgeX) {
+func CommandValuesToEventDTO(cvs []*models.CommandValue, deviceName string, sourceName string, dataTransform bool, dic *di.Container) (*dtos.Event, errors.EdgeX) {
 	// in some case device service driver implementation would generate no readings
 	// in this case no event would be created. Based on the implementation there would be 2 scenarios:
 	// 1. uninitialized *CommandValue slices, i.e. nil
@@ -46,13 +46,12 @@ func CommandValuesToEventDTO(cvs []*models.CommandValue, deviceName string, sour
 	origin := getUniqueOrigin()
 	tags := make(map[string]interface{})
 	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
-	config := container.ConfigurationFrom(dic.Get)
-	readings := make([]dtos.BaseReading, 0, config.Device.MaxCmdOps)
+	readings := make([]dtos.BaseReading, 0, len(cvs))
 	for _, cv := range cvs {
 		if cv == nil {
 			continue
 		}
-		// double check the CommandValue return from ProtocolDriver match device command
+		// double-check the CommandValue return from ProtocolDriver match device command
 		dr, ok := cache.Profiles().DeviceResource(device.ProfileName, cv.DeviceResourceName)
 		if !ok {
 			msg := fmt.Sprintf("failed to find DeviceResource %s in Device %s for CommandValue (%s)", cv.DeviceResourceName, deviceName, cv.String())
@@ -61,7 +60,7 @@ func CommandValuesToEventDTO(cvs []*models.CommandValue, deviceName string, sour
 		}
 
 		// perform data transformation
-		if config.Device.DataTransform {
+		if dataTransform {
 			edgexErr := TransformReadResult(cv, dr.Properties)
 			if edgexErr != nil {
 				lc.Errorf("failed to transform CommandValue (%s): %v", cv.String(), edgexErr)
@@ -111,6 +110,7 @@ func CommandValuesToEventDTO(cvs []*models.CommandValue, deviceName string, sour
 			return nil, errors.NewCommonEdgeXWrapper(err)
 		}
 		// ReadingUnits=true to include units in the reading
+		config := container.ConfigurationFrom(dic.Get)
 		if config.Writable.Reading.ReadingUnits {
 			reading.Units = dr.Properties.Units
 		}
diff --git a/pkg/service/async.go b/pkg/service/async.go
index 88f61518..73e0628c 100644
--- a/pkg/service/async.go
+++ b/pkg/service/async.go
@@ -12,7 +12,7 @@ import (
 	"fmt"
 	"regexp"
 
-	"github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
+	bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
 	"github.com/edgexfoundry/go-mod-bootstrap/v3/di"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/requests"
@@ -20,6 +20,7 @@ import (
 
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/common"
+	"github.com/edgexfoundry/device-sdk-go/v3/internal/container"
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/transformer"
 	sdkModels "github.com/edgexfoundry/device-sdk-go/v3/pkg/models"
 )
@@ -65,7 +66,9 @@ func (s *deviceService) sendAsyncValues(acv *sdkModels.AsyncValues, working chan
 	if len(acv.CommandValues) == 1 && acv.SourceName == "" {
 		acv.SourceName = acv.CommandValues[0].DeviceResourceName
 	}
-	event, err := transformer.CommandValuesToEventDTO(acv.CommandValues, acv.DeviceName, acv.SourceName, dic)
+
+	configuration := container.ConfigurationFrom(dic.Get)
+	event, err := transformer.CommandValuesToEventDTO(acv.CommandValues, acv.DeviceName, acv.SourceName, configuration.Device.DataTransform, dic)
 	if err != nil {
 		s.lc.Errorf("failed to transform CommandValues to Event: %v", err)
 		return
@@ -106,7 +109,7 @@ func (s *deviceService) processAsyncFilterAndAdd(ctx context.Context) {
 						}
 
 						req := requests.NewAddDeviceRequest(dtos.FromDeviceModelToDTO(device))
-						_, err := container.DeviceClientFrom(s.dic.Get).Add(ctx, []requests.AddDeviceRequest{req})
+						_, err := bootstrapContainer.DeviceClientFrom(s.dic.Get).Add(ctx, []requests.AddDeviceRequest{req})
 						if err != nil {
 							s.lc.Errorf("failed to create discovered device %s: %v", device.Name, err)
 						} else {


Test patch:
diff --git a/internal/controller/http/command_test.go b/internal/controller/http/command_test.go
index 544af2da..a3ef14fb 100644
--- a/internal/controller/http/command_test.go
+++ b/internal/controller/http/command_test.go
@@ -1,5 +1,5 @@
 //
-// Copyright (C) 2022 IOTech Ltd
+// Copyright (C) 2022-2023 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -12,6 +12,7 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"strings"
+	"sync"
 	"testing"
 
 	bootstrapContainer "github.com/edgexfoundry/go-mod-bootstrap/v3/bootstrap/container"
@@ -23,12 +24,14 @@ import (
 	commonDTO "github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/common"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/dtos/responses"
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/models"
+	messagingMocks "github.com/edgexfoundry/go-mod-messaging/v3/messaging/mocks"
 	"github.com/gorilla/mux"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
+	sdkCommon "github.com/edgexfoundry/device-sdk-go/v3/internal/common"
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/config"
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/container"
 	"github.com/edgexfoundry/device-sdk-go/v3/pkg/interfaces/mocks"
@@ -318,12 +321,13 @@ func TestRestController_GetCommand_ReturnEvent(t *testing.T) {
 }
 
 func TestRestController_SetCommand(t *testing.T) {
-	validRequest := map[string]any{testResource: "value"}
+	validRequest := map[string]any{testResource: "value", writeOnlyResource: "value"}
 	invalidRequest := map[string]any{"invalid": "test"}
 	emptyValueRequest := map[string]any{objectResource: ""}
 
 	dic := mockDic()
 
+	sdkCommon.InitializeSentMetrics(logger.NewMockClient(), dic)
 	err := cache.InitCache(testService, dic)
 	require.NoError(t, err)
 
@@ -338,8 +342,10 @@ func TestRestController_SetCommand(t *testing.T) {
 		expectedStatusCode int
 	}{
 		{"valid - device resource", testDevice, testResource, validRequest, http.StatusOK},
+		{"valid - write-only device resource", testDevice, writeOnlyResource, validRequest, http.StatusOK},
 		{"valid - device resource not specified in request body but default value provided", testDevice, testResource, invalidRequest, http.StatusOK},
 		{"valid - device command", testDevice, testCommand, validRequest, http.StatusOK},
+		{"valid - write-only device command", testDevice, writeOnlyCommand, validRequest, http.StatusOK},
 		{"valid - device command not specified in request body but default value provided", testDevice, testCommand, invalidRequest, http.StatusOK},
 		{"invalid - device name parameter is empty", "", testResource, validRequest, http.StatusBadRequest},
 		{"invalid - command is empty", testDevice, "", validRequest, http.StatusBadRequest},
@@ -363,6 +369,22 @@ func TestRestController_SetCommand(t *testing.T) {
 			req = mux.SetURLVars(req, map[string]string{common.Name: testCase.deviceName, common.Command: testCase.commandName})
 			require.NoError(t, err)
 
+			var wg sync.WaitGroup
+			if testCase.commandName != writeOnlyCommand && testCase.commandName != writeOnlyResource {
+				wg.Add(1)
+			}
+			messagingClientMock := &messagingMocks.MessageClient{}
+			messagingClientMock.On("Publish", mock.Anything, mock.Anything).Run(func(args mock.Arguments) {
+				go func() {
+					defer wg.Done()
+				}()
+			}).Return(nil)
+			dic.Update(di.ServiceConstructorMap{
+				bootstrapContainer.MessagingClientName: func(get di.Get) any {
+					return messagingClientMock
+				},
+			})
+
 			// Act
 			recorder := httptest.NewRecorder()
 			handler := http.HandlerFunc(controller.SetCommand)
@@ -378,6 +400,11 @@ func TestRestController_SetCommand(t *testing.T) {
 			assert.Equal(t, testCase.expectedStatusCode, res.StatusCode, "Response status code not as expected")
 			if testCase.expectedStatusCode == http.StatusOK {
 				assert.Empty(t, res.Message, "Message should be empty when it is successful")
+
+				wg.Wait()
+				if testCase.commandName != writeOnlyCommand && testCase.commandName != writeOnlyResource {
+					messagingClientMock.AssertNumberOfCalls(t, "Publish", 1)
+				}
 			} else {
 				assert.NotEmpty(t, res.Message, "Response message doesn't contain the error message")
 			}
diff --git a/internal/transformer/transform_test.go b/internal/transformer/transform_test.go
index 88df283a..47a77bc6 100644
--- a/internal/transformer/transform_test.go
+++ b/internal/transformer/transform_test.go
@@ -1,5 +1,5 @@
 //
-// Copyright (C) 2021 IOTech Ltd
+// Copyright (C) 2021-2023 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -168,7 +168,7 @@ func TestCommandValuesToEventDTO_ReadingUnits(t *testing.T) {
 					return configuration
 				},
 			})
-			event, err := CommandValuesToEventDTO(testCase.CommandValues, TestDevice, TestDeviceCommand, dic)
+			event, err := CommandValuesToEventDTO(testCase.CommandValues, TestDevice, TestDeviceCommand, configuration.Device.DataTransform, dic)
 			require.NoError(t, err)
 
 			assert.Equal(t, TestDevice, event.DeviceName)