Add check to omit `DateTime` from etag calculation

Ignores any json property named `DateTime` when calculating
the etag value of an HTTP response as per the updated
Redfish Spec (section 6.5: ETags)

Tested:
 - Redfish Service Validator passes
 - Tested on romulus:
1. GET resource with a "DateTime" field
```
curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/TaskService" \
    --etag-save ./etag.txt -v
...
< etag: "6A4CE897"
...
{
  "@odata.id": "/redfish/v1/TaskService",
  "@odata.type": "#TaskService.v1_1_4.TaskService",
  "CompletedTaskOverWritePolicy": "Oldest",
  "DateTime": "2025-07-23T17:08:20+00:00",
  "Id": "TaskService",
  "LifeCycleEventOnTaskStateChange": true,
  "Name": "Task Service",
  "ServiceEnabled": true,
  "Status": {
    "State": "Enabled"
  },
  "Tasks": {
    "@odata.id": "/redfish/v1/TaskService/Tasks"
  }
```

2. GET same resource again later, etag is same as before
```
curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/TaskService" \
    --etag-save ./etag.txt -v
...
< etag: "6A4CE897"
...
{
  "@odata.id": "/redfish/v1/TaskService",
  "@odata.type": "#TaskService.v1_1_4.TaskService",
  "CompletedTaskOverWritePolicy": "Oldest",
  "DateTime": "2025-07-23T17:10:48+00:00",
  "Id": "TaskService",
  "LifeCycleEventOnTaskStateChange": true,
  "Name": "Task Service",
  "ServiceEnabled": true,
  "Status": {
    "State": "Enabled"
  },
  "Tasks": {
    "@odata.id": "/redfish/v1/TaskService/Tasks"
  }
```
"DateTime" is the only value to have changed, but since
it is ignored the etag did not change

3. GET with if-none-match returns 304
```
curl -k -H "X-Auth-Token: $XAUTH_TOKEN" "https://$BMC/redfish/v1/TaskService" \
    --etag-save ./etag.txt --etag-compare ./etag.txt -v
...
> if-none-match: "6A4CE897"
...
< HTTP/2 304
< allow: GET
< odata-version: 4.0
< strict-transport-security: max-age=31536000; includeSubdomains
< pragma: no-cache
< cache-control: no-store, max-age=0
< x-content-type-options: nosniff
< etag: "6A4CE897"
< date: Wed, 23 Jul 2025 17:14:39 GMT
< content-length: 0
<
...
```

Change-Id: I51f7668e75719c69c55535e4a1e48c8bae7c9488
Signed-off-by: Corey Ethington <cethington@coreweave.com>
diff --git a/http/complete_response_fields.hpp b/http/complete_response_fields.hpp
index 41314b8..7b6a509 100644
--- a/http/complete_response_fields.hpp
+++ b/http/complete_response_fields.hpp
@@ -67,7 +67,7 @@
     BMCWEB_LOG_INFO("Response: {}", res.resultInt());
     addSecurityHeaders(res);
 
-    res.setHashAndHandleNotModified();
+    res.setResponseEtagAndHandleNotModified();
     if (res.jsonValue.is_structured())
     {
         using http_helpers::ContentType;
diff --git a/http/http2_connection.hpp b/http/http2_connection.hpp
index efd9225..d394889 100644
--- a/http/http2_connection.hpp
+++ b/http/http2_connection.hpp
@@ -321,12 +321,12 @@
                 return 0;
             }
         }
-        std::string_view expected =
+        std::string_view expectedEtag =
             thisReq.getHeaderValue(boost::beast::http::field::if_none_match);
-        BMCWEB_LOG_DEBUG("Setting expected hash {}", expected);
-        if (!expected.empty())
+        BMCWEB_LOG_DEBUG("Setting expected etag {}", expectedEtag);
+        if (!expectedEtag.empty())
         {
-            asyncResp->res.setExpectedHash(expected);
+            asyncResp->res.setExpectedEtag(expectedEtag);
         }
         handler->handle(it->second.req, asyncResp);
         return 0;
diff --git a/http/http_connection.hpp b/http/http_connection.hpp
index 64aac00..8e68bf5 100644
--- a/http/http_connection.hpp
+++ b/http/http_connection.hpp
@@ -440,11 +440,11 @@
         {
             return;
         }
-        std::string_view expected =
+        std::string_view expectedEtag =
             req->getHeaderValue(boost::beast::http::field::if_none_match);
-        if (!expected.empty())
+        if (!expectedEtag.empty())
         {
-            asyncResp->res.setExpectedHash(expected);
+            asyncResp->res.setExpectedEtag(expectedEtag);
         }
         handler->handle(req, asyncResp);
     }
diff --git a/http/http_response.hpp b/http/http_response.hpp
index 0d32982..81c664a 100644
--- a/http/http_response.hpp
+++ b/http/http_response.hpp
@@ -77,7 +77,9 @@
     Response() = default;
     Response(Response&& res) noexcept :
         response(std::move(res.response)), jsonValue(std::move(res.jsonValue)),
-        expectedHash(std::move(res.expectedHash)), completed(res.completed)
+        requestExpectedEtag(std::move(res.requestExpectedEtag)),
+        currentOverrideEtag(std::move(res.currentOverrideEtag)),
+        completed(res.completed)
     {
         // See note in operator= move handler for why this is needed.
         if (!res.completed)
@@ -102,7 +104,8 @@
         }
         response = std::move(r.response);
         jsonValue = std::move(r.jsonValue);
-        expectedHash = std::move(r.expectedHash);
+        requestExpectedEtag = std::move(r.requestExpectedEtag);
+        currentOverrideEtag = std::move(r.currentOverrideEtag);
 
         // Only need to move completion handler if not already completed
         // Note, there are cases where we might move out of a Response object
@@ -223,10 +226,21 @@
 
         jsonValue = nullptr;
         completed = false;
-        expectedHash = std::nullopt;
+        requestExpectedEtag = std::nullopt;
+        currentOverrideEtag = std::nullopt;
     }
 
-    std::string computeEtag() const
+    void setCurrentOverrideEtag(std::string_view newEtag)
+    {
+        if (currentOverrideEtag)
+        {
+            BMCWEB_LOG_WARNING(
+                "Response override etag was incorrectly set twice");
+        }
+        currentOverrideEtag = newEtag;
+    }
+
+    std::string getCurrentEtag() const
     {
         // Only set etag if this request succeeded
         if (result() != http::status::ok)
@@ -238,6 +252,12 @@
         {
             return "";
         }
+
+        if (currentOverrideEtag)
+        {
+            return currentOverrideEtag.value();
+        }
+
         size_t hashval = std::hash<nlohmann::json>{}(jsonValue);
         return "\"" + intToHexString(hashval, 8) + "\"";
     }
@@ -283,26 +303,35 @@
         return ret;
     }
 
-    void setHashAndHandleNotModified()
+    void setResponseEtagAndHandleNotModified()
     {
         // Can only hash if we have content that's valid
         if (jsonValue.empty() || result() != http::status::ok)
         {
             return;
         }
-        size_t hashval = std::hash<nlohmann::json>{}(jsonValue);
-        std::string hexVal = "\"" + intToHexString(hashval, 8) + "\"";
+        std::string hexVal = getCurrentEtag();
         addHeader(http::field::etag, hexVal);
-        if (expectedHash && hexVal == *expectedHash)
+        if (requestExpectedEtag && hexVal == *requestExpectedEtag)
         {
             jsonValue = nullptr;
             result(http::status::not_modified);
         }
     }
 
-    void setExpectedHash(std::string_view hash)
+    std::optional<std::string_view> getExpectedEtag() const
     {
-        expectedHash = hash;
+        return requestExpectedEtag;
+    }
+
+    void setExpectedEtag(std::string_view etag)
+    {
+        if (requestExpectedEtag)
+        {
+            BMCWEB_LOG_WARNING(
+                "Request expected etag was incorrectly set twice");
+        }
+        requestExpectedEtag = etag;
     }
 
     OpenCode openFile(
@@ -347,7 +376,8 @@
     }
 
   private:
-    std::optional<std::string> expectedHash;
+    std::optional<std::string> requestExpectedEtag;
+    std::optional<std::string> currentOverrideEtag;
     bool completed = false;
     std::function<void(Response&)> completeRequestHandler;
 };