Select Page

Just last week I wrote a quick blog on my Company’s portal on the integration of Sitecore Experience Platform 9 with IoT devices. I wanted to convey a message of the importance of IoT to Commence. Even though Amazon just pulled a plug on their IoT Dush button in lieu of Amazon Alexa, I am a strong believer that Commerce IoT is here to stay. The Button may not be the best solution for ordering Cats Food, but a small business may benefit greatly and even save money by using an automated inventory replenishment process as an example.

My goal is to demonstrate that connecting IoT Device with Commerce platform is no longer the privilege of companies like Amazon but can be done even by the smallest commerce provider. In my solution, I want to follow the latest and greatest design principals, based on microservice and event-driven architecture. These principals are an evolution of OOP, with even greater support for decoupling. I selected Sitecore XC 9, which is written in .NET Core and designed using Microservice principles. I selected Azure to handle IoT logic as it has all core components I need to make smooth integration with Sitecore XC9. Now, I’ll use Serverless architecture of Azure to tie my Commerce Server with IoT as shown on this diagram:

So, let’s get started. First, I’ll create a Sitecore Microservice Plugin to allow placing orders over a single endpoint. Please note, I did not follow Sitecore best practice of creating blocks, entities for persistence and interfaces for reusability nor I did address federated payment logic. My goal was to create secure order orchestration endpoint using Sitecore Commerce Engine.

public class AddCustomOrderController : CommerceController
    {
        public AddCustomOrderController(IServiceProvider serviceProvider, CommerceEnvironment globalEnvironment)
            : base(serviceProvider, globalEnvironment)
        {
        }
        [HttpPut]
        [Route("AddCustomOrder")]
        public async Task<IActionResult> Put([FromBody] ODataActionParameters value)
        {
            if (!this.ModelState.IsValid || value == null)
                return (IActionResult)new BadRequestObjectResult((object)value);

            string id = value["customerId"].ToString();

            if (!this.ModelState.IsValid || string.IsNullOrEmpty(id))
                return (IActionResult)this.NotFound();


            GetCustomerCommand command0 = this.Command<GetCustomerCommand>();

            Customer customer = await command0.Process(this.CurrentContext, id);
            var y = customer.GetComponent<AddressComponent>();


            //refactor
            string randoms = Guid.NewGuid().ToString().Replace("-", string.Empty).Replace("+", string.Empty);

            string cartId = string.Format("{0}{1}", "CUSTOM", randoms);
            string str = value["itemId"].ToString();
            Decimal result;
            string q = "1";



            if (!Decimal.TryParse(q, out result))
                return (IActionResult)new BadRequestObjectResult((object)q);


            AddCartLineCommand command = this.Command<AddCartLineCommand>();
            CartLineComponent line = new CartLineComponent()
            {
                ItemId = str,
                Quantity = result
            };
            Cart cart = await command.Process(this.CurrentContext, cartId, line);

            //FulfillmentComponent

            FulfillmentComponent fulfillment = (PhysicalFulfillmentComponent)new PhysicalFulfillmentComponent()
            {
                ShippingParty = new Party()
                {
                    Address1 = y.Party.Address1,
                    City = y.Party.City,
                    ZipPostalCode = y.Party.ZipPostalCode,
                    State = y.Party.State,
                    StateCode = y.Party.StateCode,
                    CountryCode = y.Party.CountryCode,
                    AddressName = y.Party.AddressName,
                    Name = y.Party.Name

                },
                FulfillmentMethod = new EntityReference()
                {
                    Name = "Ground",
                    EntityTarget = "B146622D-DC86-48A3-B72A-05EE8FFD187A"
                }
            };

            SetCartFulfillmentCommand _CartFulfillmentCommand = this.Command<SetCartFulfillmentCommand>();
            Cart cart1 = await _CartFulfillmentCommand.Process(CurrentContext, cartId, fulfillment);


            //FederatedPaymentComponent

            decimal gt;
            Decimal.TryParse(cart1.Totals.GrandTotal.Amount.ToString(), out gt);

            FederatedPaymentComponent paymentComponent = new FederatedPaymentComponent(new Money() { Amount = gt });

            paymentComponent.PaymentMethod = new EntityReference()
            {
                EntityTarget = "0CFFAB11-2674-4A18-AB04-228B1F8A1DEC",
                Name = "Federated"
            };
       
            paymentComponent.PaymentMethodNonce = "fake-valid-nonce";

            paymentComponent.BillingParty = new Party()
            {

                Address1 = y.Party.Address1,
                City = y.Party.City,
                ZipPostalCode = y.Party.ZipPostalCode,
                State = y.Party.State,
                StateCode = y.Party.StateCode,
                CountryCode = y.Party.CountryCode,
                AddressName = y.Party.AddressName,

                //FirstName = y.Party.FirstName,
                //LastName = y.Party.LastName
            };

            AddPaymentsCommand _PaymentsCommand = this.Command<AddPaymentsCommand>();
            Cart cart2 = await _PaymentsCommand.Process(this.CurrentContext, cartId, (IEnumerable<PaymentComponent>)new List<PaymentComponent>()
              {
                (PaymentComponent) paymentComponent
              });

            //CreateOrderCommand


            string email = customer.Email;


            CreateOrderCommand _CreateOrderCommand = this.Command<CreateOrderCommand>();


            Order order = await _CreateOrderCommand.Process(this.CurrentContext, cartId, email);



            return (IActionResult)new ObjectResult((object)_CreateOrderCommand);


        }
    }

Now, we are done with Sitecore development, and we will create IoT Logic. To do so, we will use IoT DevKit – an Arduino based device certified for Azure. In this example, we will use button events to place an order. To build code for this device, I am using Visual Studio Code with Azure IoT Device Workbench extension. The complexity of connecting your IoT Device with Azure cloud is abstracted by DevKitMQTTClient_SendEvent method. This method sends machine-to-machine, extremely lightweight messages(MQTT), to Azure IoT Hub. The DevKitMQTTClient_Check monitor response pipeline and updates IoT Device screen with an order ID.

#include "AZ3166WiFi.h"
#include "AzureIotHub.h"
#include "DevKitMQTTClient.h"
#include "OledDisplay.h"
#include "Sensor.h"
#include "SitecoreXC9UI.h"


//TODO refactor to pull order/customer details from Sitecore
static  char* iot_event = "{\"product\":\"Habitat_Master|6042058|56042058\", \"customer\": \"Entity-Customer-e6619c849bab42339e3f29e73137952d\"}";

// Application running status
// 0 - idle
// 1 - work
static int app_status;
static int order_status;

static RGB_LED rgbLed;
// Indicate whether WiFi is ready
static bool hasWifi = false;
// Indicate whether IoT Hub is ready
static bool hasIoTHub = false;

static void InitWiFi()
{
  Screen.clean();
  DrawLogoBB("Sitecore IoT");

  if (WiFi.begin() == WL_CONNECTED)
  {
    IPAddress ip = WiFi.localIP();
    hasWifi = true;
  }
  else
  {
    hasWifi = false;
  }
}

static void OrderProgress()
{
  if (order_status == 1)
  {
    DrawAppTitle("Order Status");
    Screen.print(1, "   Received");
    Screen.print(2, "   Processing");
    Screen.print(3, "   Completed");
  }
  DrawCheckBox(1, 0, (order_status >= 1) ? 1 : 0);
  DrawCheckBox(2, 0, (order_status >= 2) ? 1 : 0);
  DrawCheckBox(3, 0, (order_status >= 3) ? 1 : 0);
  
 switch(order_status)
    {
      case 1:
           rgbLed.setColor(255, 0, 0);  // purple
        break;
     
      case 2:
            rgbLed.setColor(0, 0, 255);  // blue
        break;
        
     case 3:
            rgbLed.setColor(0, 255, 0);  // green
        break;
    }

  delay(500);
}

///////////////////////////////////////////////////////////////////////
// Callback functions
static void xc9CallBack(const char *payLoad, int size)
{
  order_status  = 3;
  OrderProgress();
  Screen.clean();
  DrawAppTitle("Order:");
  Screen.print(1, &payLoad[0], true);

 delay(2000);

}
///////////////////////////////////////////////////////////////////////
// Actions
static void StartApp()
{
    if (digitalRead(USER_BUTTON_A) == LOW)
    {

      app_status = 1;
      order_status = 0;

    }
    // Check with the IoT hub
    DevKitMQTTClient_Check();

}
static void CallSitecore()
{
    order_status = 1;
    // LED
    DigitalOut LedUser(LED_BUILTIN);
    LedUser = 1;

    // Update the screen
    OrderProgress();
    // Send to IoT hub
    if (DevKitMQTTClient_SendEvent(iot_event))
    {
      order_status =2;
      OrderProgress();
    }
    else
    {
      // Failed to send message to IoT hub
    }
      LedUser = 0;
      app_status = 0;
}

///////////////////////////////////////////////////////////////////////
// Arduino sketch
void setup()
{

  app_status = 0;
  order_status = 0;

  Screen.init();
  DrawLogoBB("Sitecore IoT");
  Serial.begin(115200);
  

  hasWifi = false;
  InitWiFi();
  if (!hasWifi)
  {
    return;
  }

  rgbLed.turnOff();

  pinMode(USER_BUTTON_A, INPUT);
  pinMode(USER_BUTTON_B, INPUT);
  pinMode(LED_BUILTIN, OUTPUT);
  
  
  if (!DevKitMQTTClient_Init())
  {
    Screen.clean();
    Screen.print(2, "No IoT Hub");
    hasIoTHub = false;
    return;
  }
  hasIoTHub = true;

  DevKitMQTTClient_SetOption(OPTION_MINI_SOLUTION_NAME, "Sc9Function");
  DevKitMQTTClient_SetMessageCallback(xc9CallBack);

  rgbLed.setColor(0, 0, 0);
  

  Screen.clean();
  DrawAppTitle("Sitecore IoT");
  Screen.print(1, "");
  Screen.print(2, "< Press to Order");
  Screen.print(3, "");
}

void loop()
{
  if (hasWifi && hasIoTHub)
  {
    switch(app_status)
    {
      case 0:
        StartApp();
        break;
      case 1:
        CallSitecore();
        break;

    }
  }
  delay(100);
}

And lastly, we will create a ‘glue’ – integration logic to connect or IoT device with Sitecore. Thanks to Azure Serverless platform, we would not need to write a lot of code, introduce bugs or write Unit Tests. The only code we need to write is to process messages, generated by our IoT device and route requests to our Sitecore Commerce Engine. The Azure Function is an excellent choice for processing IoT Hub messages and performing necessary actions(see code below).

namespace IoTWorkbench
{
    public class DeviceObject
    {
        public string product;
        public string customer;
    }
    public class Sc9FunctionException : Exception
    {
        public Sc9FunctionException(string message)
            : base($"Sc9Function:{message}")
        {
        }
    }

    public static class Sc9Function
    {

        [FunctionName("IoTHubXC9")]
        public static void Run([IoTHubTrigger("%eventHubConnectionPath%", Connection = "eventHubConnectionString")]EventData message, ILogger log)
        {
            // TODOļ¼š parameterize  the device name.
            string deviceId = "device2";
            string myEventHubMessage = Encoding.UTF8.GetString(message.Body.Array);
            log.LogInformation($"C# IoT Hub trigger function processed a message: {myEventHubMessage}");

            // Get the hash tag
            string product = string.Empty;
            string customer = string.Empty;
            try
            {
                DeviceObject deviceObject = Newtonsoft.Json.JsonConvert.DeserializeObject<DeviceObject>(myEventHubMessage);
                if (String.IsNullOrEmpty(deviceObject.product))
                {
                    // No hash tag or this is a heartbeat package
                    return;
                }
                product = deviceObject.product;
                customer = deviceObject.customer;

                log.LogInformation($"product: {product}");
                log.LogInformation($"customer: {customer}");
            }
            catch (Exception ex)
            {
                throw new Sc9FunctionException($"Failed to deserialize message:{myEventHubMessage}. Error detail: {ex.Message}");
            }



            string scMessage = "Try Again";
            if (!String.IsNullOrEmpty(product) && !String.IsNullOrEmpty(customer))
            {

                string msg = "{\"itemId\": \"" + product + "\", \"customerId\": \"" + customer + "\"}";
                log.LogInformation($"Message: {msg}");

                scMessage = CreateOrder(msg).Result;

            }
            try
            {
                string connectionString = System.Environment.GetEnvironmentVariable("iotHubConnectionString");
                using (ServiceClient serviceClient = ServiceClient.CreateFromConnectionString(connectionString))
                {
                    log.LogInformation($"Response: {scMessage}");
                    Message commandMessage = new Message(Encoding.UTF8.GetBytes(scMessage));
                    serviceClient.SendAsync(deviceId, commandMessage).Wait();

                }
                log.LogInformation($"Response: {scMessage}");
            }
            catch (Exception ex)
            {
                throw new Sc9FunctionException($"Failed to send C2D message: {ex.Message}");
            }

        }
        private static async Task<AuthResponse> AuthToken()
        {
            HttpClient client = new HttpClient();
            try
            {
                string url = "https://store.xxxxx.com:5050/connect/token";
                string body = "password=xxx&username=sitecore\\xxx&client_id=postman-api&grant_type=password&scope=openid EngineAPI postman_api";
                client.DefaultRequestHeaders.Accept.Clear();
                //client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                var payload = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
                var response = await client.PostAsync(url, payload);
                var x = await response.Content.ReadAsStringAsync();
                var t = Task.Run(() => x);
                t.Wait();
                AuthResponse auth = JsonConvert.DeserializeObject<AuthResponse>(t.Result);

                return auth;
            }
            catch (Exception ex)
            {

                return null;
            }
        }
        static async Task<string> CreateOrder(string body)
        {
            HttpClient client = new HttpClient();
            try
            {
                string url = "https://store.xxxxx.com:5000/api/AddCustomOrder";

                client.DefaultRequestHeaders.Accept.Clear();

                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                client.DefaultRequestHeaders.Add("Authorization", string.Format("Bearer {0}", AuthToken().Result.access_token));
                client.DefaultRequestHeaders.Add("Environment", "HabitatAuthoring");
                client.DefaultRequestHeaders.Add("ShopName", "Storefront");


                var payload = new StringContent(body, Encoding.UTF8, "application/json");
                var response = await client.PutAsync(url, payload);
                var x = await response.Content.ReadAsStringAsync();
                var t = Task.Run(() => x);
                t.Wait();


                var m = Regex.Match(t.Result, @".*""OrderId"":""(.*?)"".*");
                Console.WriteLine(m.Groups[1].Value);


                return m.Groups[1].Value;
            }
            catch (Exception ex)
            {

                return ex.Message;
            }
        }
        public class AuthResponse
        {
            public string access_token { get; set; }

        }
    }

I hope I convinced you that doing IoT with XC9 is easy! What took me a weekend to develop now, could’ve taken me months just a few years back. Thanks to Microsoft Azure and Modular Sitecore XC9 I can deliver world-class scalable commerce applications with a minimal effort.