Add Persistent Heartbeat subscription properties

This adds Heartbeat parameters to subscriptions so that the future
heartbeat implementation can use those parameters specified by the
schema [1][2][3].
- SendHeartbeat
- HeartbeatIntervalMinutes

Tested:

1. POST Subscription
- Create a subscription (e.g. via Redfish-Event-Listener) or like

```
curl -k  -H "Content-Type: application/json" -X POST https://${bmc}/redfish/v1/EventService/Subscriptions \
 -d '{
  "Context": "Public",
  "DeliveryRetryPolicy": "TerminateAfterRetries",
  "Destination": "https://DESTINATION-IPADDR/Redfish-Evt-Listener",
  "EventFormatType": "Event",
  "HeartbeatIntervalMinutes": 2,
  "HttpHeaders": [],
  "MessageIds": [],
  "MetricReportDefinitions": [],
  "Protocol": "Redfish",
  "RegistryPrefixes": [],
  "ResourceTypes": [],
  "SendHeartbeat": true,
  "SubscriptionType": "RedfishEvent",
  "VerifyCertificate": true
}'
```

2. GET the subscription and check the content
```
SUBID=<id>
curl -k -X GET https://${bmc}/redfish/v1/EventService/Subscriptions/${SUBID}
```

3. PATCH Subscription
- PATCH with various SendHeartbeat & HeartbeatIntervalMinutes

For example,
```
curl -k -X PATCH https://${bmc}/redfish/v1/EventService/Subscriptions/${SUBID} \
    -H "Content-Type: application/json" \
     -d '{"SendHeartbeat":true, "HeartbeatIntervalMinutes":10}'
```

- Restart bmcweb or reboot BMC

- Get the subscription data and see whether the heartbeat properties are
  persistent.

4. Redfish Validator Service passes

[1] https://github.com/openbmc/bmcweb/blob/d109e2b60f7bb367dc8115475c6cb86bca6e1914/redfish-core/schema/dmtf/json-schema/EventDestination.v1_15_0.json#L356
[2] https://github.com/openbmc/bmcweb/blob/d109e2b60f7bb367dc8115475c6cb86bca6e1914/redfish-core/schema/dmtf/json-schema/EventDestination.v1_15_0.json#L222
[3] https://www.dmtf.org/sites/default/files/standards/documents/DSP2046_2022.3.html

Change-Id: I9e7feadb2e851ca320147df2231f65ece58ddf25
Signed-off-by: Myung Bae <myungbae@us.ibm.com>
diff --git a/Redfish.md b/Redfish.md
index bc41ae5..cd5b726 100644
--- a/Redfish.md
+++ b/Redfish.md
@@ -492,9 +492,11 @@
 - Destination
 - EventTypes
 - Context
+- HeartbeatIntervalMinutes
 - OriginResources
 - RegistryPrefixes
 - Protocol
+- SendHeartbeat
 
 ### /redfish/v1/JsonSchemas/
 
diff --git a/include/event_service_store.hpp b/include/event_service_store.hpp
index b0a1d97..ffbb389 100644
--- a/include/event_service_store.hpp
+++ b/include/event_service_store.hpp
@@ -7,6 +7,7 @@
 #include <boost/url/url.hpp>
 #include <nlohmann/json.hpp>
 
+#include <limits>
 #include <memory>
 #include <string>
 #include <vector>
@@ -22,6 +23,10 @@
     std::string protocol;
     bool verifyCertificate = true;
     std::string retryPolicy;
+    bool sendHeartbeat = false;
+    // This value of hbIntervalMinutes is just a reasonable default value and
+    // most clients will update it if sendHeartbeat is turned on
+    uint64_t hbIntervalMinutes = 10;
     std::string customText;
     std::string eventFormatType;
     std::string subscriptionType;
@@ -93,6 +98,25 @@
                 }
                 subvalue.retryPolicy = *value;
             }
+            else if (element.first == "SendHeartbeat")
+            {
+                const bool* value = element.second.get_ptr<const bool*>();
+                if (value == nullptr)
+                {
+                    continue;
+                }
+                subvalue.sendHeartbeat = *value;
+            }
+            else if (element.first == "HeartbeatIntervalMinutes")
+            {
+                const uint64_t* value =
+                    element.second.get_ptr<const uint64_t*>();
+                if (value == nullptr || *value < 1 || *value > 65535)
+                {
+                    continue;
+                }
+                subvalue.hbIntervalMinutes = *value;
+            }
             else if (element.first == "Context")
             {
                 const std::string* value =
diff --git a/include/persistent_data.hpp b/include/persistent_data.hpp
index afcdb14..0cdedcd 100644
--- a/include/persistent_data.hpp
+++ b/include/persistent_data.hpp
@@ -326,6 +326,9 @@
             subscription["Id"] = subValue.id;
             subscription["Context"] = subValue.customText;
             subscription["DeliveryRetryPolicy"] = subValue.retryPolicy;
+            subscription["SendHeartbeat"] = subValue.sendHeartbeat;
+            subscription["HeartbeatIntervalMinutes"] =
+                subValue.hbIntervalMinutes;
             subscription["Destination"] = subValue.destinationUrl;
             subscription["EventFormatType"] = subValue.eventFormatType;
             subscription["HttpHeaders"] = std::move(headers);
diff --git a/redfish-core/lib/event_service.hpp b/redfish-core/lib/event_service.hpp
index d415496..04ca735 100644
--- a/redfish-core/lib/event_service.hpp
+++ b/redfish-core/lib/event_service.hpp
@@ -34,6 +34,7 @@
 
 #include <charconv>
 #include <memory>
+#include <optional>
 #include <ranges>
 #include <span>
 #include <string>
@@ -299,6 +300,8 @@
             std::optional<std::string> subscriptionType;
             std::optional<std::string> eventFormatType2;
             std::optional<std::string> retryPolicy;
+            std::optional<bool> sendHeartbeat;
+            std::optional<uint64_t> hbIntervalMinutes;
             std::optional<std::vector<std::string>> msgIds;
             std::optional<std::vector<std::string>> regPrefixes;
             std::optional<std::vector<std::string>> originResources;
@@ -312,6 +315,7 @@
                     "DeliveryRetryPolicy", retryPolicy, //
                     "Destination", destUrl, //
                     "EventFormatType", eventFormatType2, //
+                    "HeartbeatIntervalMinutes", hbIntervalMinutes, //
                     "HttpHeaders", headers, //
                     "MessageIds", msgIds, //
                     "MetricReportDefinitions", mrdJsonArray, //
@@ -319,6 +323,7 @@
                     "Protocol", protocol, //
                     "RegistryPrefixes", regPrefixes, //
                     "ResourceTypes", resTypes, //
+                    "SendHeartbeat", sendHeartbeat, //
                     "SubscriptionType", subscriptionType, //
                     "VerifyCertificate", verifyCertificate //
                     ))
@@ -403,6 +408,18 @@
                                                     "RetryPolicy", "Protocol");
                     return;
                 }
+                if (sendHeartbeat)
+                {
+                    messages::propertyValueConflict(
+                        asyncResp->res, "SendHeartbeat", "Protocol");
+                    return;
+                }
+                if (hbIntervalMinutes)
+                {
+                    messages::propertyValueConflict(
+                        asyncResp->res, "HeartbeatIntervalMinutes", "Protocol");
+                    return;
+                }
                 if (msgIds)
                 {
                     messages::propertyValueConflict(asyncResp->res,
@@ -646,6 +663,21 @@
                 // Default "TerminateAfterRetries"
                 subValue->userSub->retryPolicy = "TerminateAfterRetries";
             }
+            if (sendHeartbeat)
+            {
+                subValue->userSub->sendHeartbeat = *sendHeartbeat;
+            }
+            if (hbIntervalMinutes)
+            {
+                if (*hbIntervalMinutes < 1 || *hbIntervalMinutes > 65535)
+                {
+                    messages::propertyValueOutOfRange(
+                        asyncResp->res, *hbIntervalMinutes,
+                        "HeartbeatIntervalMinutes");
+                    return;
+                }
+                subValue->userSub->hbIntervalMinutes = *hbIntervalMinutes;
+            }
 
             if (mrdJsonArray)
             {
@@ -730,6 +762,8 @@
 
                 jVal["MessageIds"] = userSub.registryMsgIds;
                 jVal["DeliveryRetryPolicy"] = userSub.retryPolicy;
+                jVal["SendHeartbeat"] = userSub.sendHeartbeat;
+                jVal["HeartbeatIntervalMinutes"] = userSub.hbIntervalMinutes;
                 jVal["VerifyCertificate"] = userSub.verifyCertificate;
 
                 nlohmann::json::array_t mrdJsonArray;
@@ -766,6 +800,8 @@
 
                 std::optional<std::string> context;
                 std::optional<std::string> retryPolicy;
+                std::optional<bool> sendHeartbeat;
+                std::optional<uint64_t> hbIntervalMinutes;
                 std::optional<bool> verifyCertificate;
                 std::optional<std::vector<nlohmann::json::object_t>> headers;
 
@@ -773,7 +809,9 @@
                         req, asyncResp->res, //
                         "Context", context, //
                         "DeliveryRetryPolicy", retryPolicy, //
+                        "HeartbeatIntervalMinutes", hbIntervalMinutes, //
                         "HttpHeaders", headers, //
+                        "SendHeartbeat", sendHeartbeat, //
                         "VerifyCertificate", verifyCertificate //
                         ))
                 {
@@ -821,6 +859,22 @@
                     subValue->userSub->retryPolicy = *retryPolicy;
                 }
 
+                if (sendHeartbeat)
+                {
+                    subValue->userSub->sendHeartbeat = *sendHeartbeat;
+                }
+                if (hbIntervalMinutes)
+                {
+                    if (*hbIntervalMinutes < 1 || *hbIntervalMinutes > 65535)
+                    {
+                        messages::propertyValueOutOfRange(
+                            asyncResp->res, *hbIntervalMinutes,
+                            "HeartbeatIntervalMinutes");
+                        return;
+                    }
+                    subValue->userSub->hbIntervalMinutes = *hbIntervalMinutes;
+                }
+
                 if (verifyCertificate)
                 {
                     subValue->userSub->verifyCertificate = *verifyCertificate;