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

Issue:
Send out only changed values if the autoEvent onChange is true
<!--🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅

Oh hi there! 😄 

To expedite issue processing please search open and closed issues before submitting a new one.
Existing issues often contain information about workarounds, resolution, or progress updates.

Please do not remove sections of this template when submitting your issue.  If the section is not relevant to your issue, leave the section but enter "n/a" for not applicable.

Note some sections are marked "REQUIRED" and must be provided.  Never mark required sections "n/a".

🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅🔅-->


# 🚀 Feature Request

### Relevant Package [**REQUIRED**]
<!-- Can you pin-point one or more EdgeX services the are relevant for this feature request? -->
<!-- ✍️edit: --> This feature request is for device services


### Description [**REQUIRED**]
<!-- ✍️--> 
The whole event with all readings will be sent out in Go Device SDK if one of the reading values is changed, when the autoEvent onChange is true.
It is designed for the system which always needs all the values, but for some systems, this just adds unecessary burden to the system.
https://github.com/edgexfoundry/device-sdk-go/blob/1736bffb6f9249122deed05ee7f896dde1cd3714/internal/autoevent/executor.go#L68-L91

### Describe the solution you'd like
Add a configuration to control this behavior to allow to only send out the changed values if the autoEvent onChange is true



Pull request description:
feat: Add SendChangedReadingsOnly config to only send updated values
Resolves #1705. Add SendChangedReadingsOnly config to only send updated auto event readings if the value is true.

<!-- 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?)
- [ ] 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? -->

## 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/example/cmd/device-simple/res/configuration.yaml b/example/cmd/device-simple/res/configuration.yaml
index 46810684..4b50c262 100644
--- a/example/cmd/device-simple/res/configuration.yaml
+++ b/example/cmd/device-simple/res/configuration.yaml
@@ -26,6 +26,10 @@ Device:
   Discovery:
     Enabled: false
     Interval: "30s"
+  AutoEvents:
+    # If set to true, only updated readings compared to the previous event are included in the generated auto event
+    SendChangedReadingsOnly: false
+
 # Example structured custom configuration
 SimpleCustom:
   OnImageLocation: ./res/on.png
diff --git a/internal/autoevent/executor.go b/internal/autoevent/executor.go
index febc853b..49306917 100644
--- a/internal/autoevent/executor.go
+++ b/internal/autoevent/executor.go
@@ -25,6 +25,7 @@ import (
 
 	"github.com/edgexfoundry/device-sdk-go/v4/internal/application"
 	sdkCommon "github.com/edgexfoundry/device-sdk-go/v4/internal/common"
+	"github.com/edgexfoundry/device-sdk-go/v4/internal/container"
 
 	"github.com/spf13/cast"
 )
@@ -34,7 +35,8 @@ type Executor struct {
 	sourceName        string
 	onChange          bool
 	onChangeThreshold float64
-	lastReadings      map[string]interface{}
+	lastReadings      map[string]any
+	onChangeReadings  []dtos.BaseReading
 	duration          time.Duration
 	stop              bool
 	mutex             *sync.Mutex
@@ -48,6 +50,7 @@ func (e *Executor) Run(ctx context.Context, wg *sync.WaitGroup, buffer chan bool
 
 	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
 	deadline := time.Now().Add(e.duration)
+	config := container.ConfigurationFrom(dic.Get)
 
 	for {
 		select {
@@ -79,6 +82,15 @@ func (e *Executor) Run(ctx context.Context, wg *sync.WaitGroup, buffer chan bool
 				if err := e.pool.Submit(func() {
 					buffer <- true
 					correlationId := uuid.NewString()
+
+					// Protect e.onChangeReadings with mutex to avoid concurrent access
+					e.mutex.Lock()
+					if e.onChange && config.Device.AutoEvents.SendChangedReadingsOnly && len(e.onChangeReadings) != 0 {
+						// Update the auto event to include only the readings that have changed.
+						evt.Readings = e.onChangeReadings
+					}
+					e.mutex.Unlock()
+
 					sdkCommon.SendEvent(evt, correlationId, dic)
 					lc.Tracef("AutoEvent - Sent new Event/Reading for '%s' source with Correlation Id '%s'", evt.SourceName, correlationId)
 					<-buffer
@@ -114,6 +126,9 @@ func (e *Executor) compareReadings(readings []dtos.BaseReading) bool {
 	}
 
 	var result = true
+	// Reset the onChangeReadings for each auto event
+	e.onChangeReadings = nil
+
 	for _, reading := range readings {
 		if lastReading, ok := e.lastReadings[reading.ResourceName]; ok {
 			switch reading.ValueType {
@@ -122,6 +137,7 @@ func (e *Executor) compareReadings(readings []dtos.BaseReading) bool {
 				if lastReading != checksum {
 					e.lastReadings[reading.ResourceName] = checksum
 					result = false
+					e.onChangeReadings = append(e.onChangeReadings, reading)
 				}
 			case common.ValueTypeUint8, common.ValueTypeUint16, common.ValueTypeUint32, common.ValueTypeUint64,
 				common.ValueTypeInt8, common.ValueTypeInt16, common.ValueTypeInt32, common.ValueTypeInt64,
@@ -130,11 +146,13 @@ func (e *Executor) compareReadings(readings []dtos.BaseReading) bool {
 				if math.Abs(t) > e.onChangeThreshold {
 					e.lastReadings[reading.ResourceName] = reading.Value
 					result = false
+					e.onChangeReadings = append(e.onChangeReadings, reading)
 				}
 			default:
 				if lastReading != reading.Value {
 					e.lastReadings[reading.ResourceName] = reading.Value
 					result = false
+					e.onChangeReadings = append(e.onChangeReadings, reading)
 				}
 			}
 		} else {
diff --git a/internal/config/types.go b/internal/config/types.go
index e05adaea..f07e672d 100644
--- a/internal/config/types.go
+++ b/internal/config/types.go
@@ -57,6 +57,8 @@ type DeviceInfo struct {
 	AllowedFails uint
 	// DeviceDownTimeout specifies the duration in seconds that the Device Service will try to contact a device if it is marked as down.
 	DeviceDownTimeout uint
+	// AutoEvents defines the configuration related to the generated auto events for the device service
+	AutoEvents AutoEventInfo
 }
 
 // DiscoveryInfo is a struct which contains configuration of device auto discovery.
@@ -77,3 +79,8 @@ type Telemetry struct {
 	Frees,
 	LiveObjects uint64
 }
+
+type AutoEventInfo struct {
+	// If true, only updated readings compared to the previous event are included in the generated auto event
+	SendChangedReadingsOnly bool
+}


Test patch:
diff --git a/internal/autoevent/executor_test.go b/internal/autoevent/executor_test.go
index 73fb0181..3a49a284 100644
--- a/internal/autoevent/executor_test.go
+++ b/internal/autoevent/executor_test.go
@@ -59,23 +59,25 @@ func TestCompareReadings(t *testing.T) {
 	readingBinaryValueUnchanged := readingsBinaryValueChanged
 
 	tests := []struct {
-		name     string
-		reading  []dtos.BaseReading
-		expected bool
+		name                   string
+		reading                []dtos.BaseReading
+		expected               bool
+		expOnChangeReadingsLen int
 	}{
-		{"false - lastReadings are nil", firstReadings, false},
-		{"false - reading's value changed", readingsValueChanged, false},
-		{"false - reading's resource name changed", readingsResourceChanged, false},
-		{"true - readings unchanged", readingsValueUnchanged, true},
-		{"false - readings length changed", readingsLengthChanged, false},
-		{"false - reading's binary value changed", readingsBinaryValueChanged, false},
-		{"true - readings unchanged", readingBinaryValueUnchanged, true},
+		{"false - lastReadings are nil", firstReadings, false, 0},
+		{"false - reading's value changed", readingsValueChanged, false, 1},
+		{"false - reading's resource name changed", readingsResourceChanged, false, 0},
+		{"true - readings unchanged", readingsValueUnchanged, true, 0},
+		{"false - readings length changed", readingsLengthChanged, false, 0},
+		{"false - reading's binary value changed", readingsBinaryValueChanged, false, 1},
+		{"true - readings unchanged", readingBinaryValueUnchanged, true, 0},
 	}
 
 	for _, testCase := range tests {
 		t.Run(testCase.name, func(t *testing.T) {
 			res := e.compareReadings(testCase.reading)
 			assert.Equal(t, testCase.expected, res, "compareReading result not as expected")
+			assert.Equal(t, testCase.expOnChangeReadingsLen, len(e.onChangeReadings), "OnChangeReadings length not as expected")
 		})
 	}
 }