
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
#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).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
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.
Recent Comments