|
| 1 | +using Microsoft.AspNetCore.WebUtilities; |
| 2 | +using Newtonsoft.Json; |
| 3 | +using Newtonsoft.Json.Linq; |
| 4 | +using Opc.Ua.Cloud.Client.Models; |
| 5 | +using System.Net.Http.Headers; |
| 6 | +using System.Text; |
| 7 | + |
| 8 | +namespace Opc.Ua.Data.Processor |
| 9 | +{ |
| 10 | + public class BatteryPassProcessor |
| 11 | + { |
| 12 | + private readonly ADXDataService _adxDataService = new ADXDataService(); |
| 13 | + private readonly HttpClient _webClient = new HttpClient() |
| 14 | + { |
| 15 | + BaseAddress = new Uri(Environment.GetEnvironmentVariable("UA_CLOUD_LIBRARY_URL")), |
| 16 | + DefaultRequestHeaders = |
| 17 | + { |
| 18 | + Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("UA_CLOUD_LIBRARY_USERNAME") + ":" + Environment.GetEnvironmentVariable("UA_CLOUD_LIBRARY_PASSWORD")))) |
| 19 | + } |
| 20 | + }; |
| 21 | + |
| 22 | + public BatteryPassProcessor() |
| 23 | + { |
| 24 | + _adxDataService.Connect(); |
| 25 | + } |
| 26 | + |
| 27 | + public void Process() |
| 28 | + { |
| 29 | + // we have two production lines in the manufacturing ontologies production line simulation and they are connected like so: |
| 30 | + // assembly -> test -> packaging |
| 31 | + CalcPCFForProductionLine("Munich", "48.1375", "11.575", 6); |
| 32 | + CalcPCFForProductionLine("Seattle", "47.609722", "-122.333056", 10); |
| 33 | + } |
| 34 | + |
| 35 | + private void CalcPCFForProductionLine(string productionLineName, string latitude, string longitude, int idealCycleTime) |
| 36 | + { |
| 37 | + try |
| 38 | + { |
| 39 | + // first of all, retrieve carbon intensity for the location of the production line |
| 40 | + CarbonIntensityQueryResult currentCarbonIntensity = WattTimeClient.GetCarbonIntensity(latitude, longitude).GetAwaiter().GetResult(); |
| 41 | + if ((currentCarbonIntensity != null) && (currentCarbonIntensity.data.Length > 0)) |
| 42 | + { |
| 43 | + // check if a new product was produced (last machine in the production line, i.e. packaging, is in state 2 ("done") with a passed QA) |
| 44 | + // and get the products serial number and energy consumption at that time |
| 45 | + Dictionary<string, object> latestProductProduced = ADXQueryForSpecificValue("packaging", productionLineName, "Status", 2); |
| 46 | + if ((latestProductProduced != null) && (latestProductProduced.Count > 0)) |
| 47 | + { |
| 48 | + Dictionary<string, object> serialNumberResult = ADXQueryForSpecificTime("packaging", productionLineName, "ProductSerialNumber", ((DateTime)latestProductProduced["Timestamp"]).ToString("yyyy-MM-dd HH:mm:ss"), idealCycleTime); |
| 49 | + double serialNumber = (double)serialNumberResult["OPCUANodeValue"]; |
| 50 | + |
| 51 | + Dictionary<string, object> timeItWasProducedPackaging = ADXQueryForSpecificValue("packaging", productionLineName, "ProductSerialNumber", serialNumber); |
| 52 | + Dictionary<string, object> energyPackaging = ADXQueryForSpecificTime("packaging", productionLineName, "EnergyConsumption", ((DateTime)timeItWasProducedPackaging["Timestamp"]).ToString("yyyy-MM-dd HH:mm:ss"), idealCycleTime); |
| 53 | + |
| 54 | + // check each other machine for the time when the product with this serial number was in the machine and get its energy comsumption at that time |
| 55 | + Dictionary<string, object> timeItWasProducedTest = ADXQueryForSpecificValue("test", productionLineName, "ProductSerialNumber", serialNumber); |
| 56 | + Dictionary<string, object> energyTest = ADXQueryForSpecificTime("test", productionLineName, "EnergyConsumption", ((DateTime)timeItWasProducedTest["Timestamp"]).ToString("yyyy-MM-dd HH:mm:ss"), idealCycleTime); |
| 57 | + |
| 58 | + Dictionary<string, object> timeItWasProducedAssembly = ADXQueryForSpecificValue("assembly", productionLineName, "ProductSerialNumber", serialNumber); |
| 59 | + Dictionary<string, object> energyAssembly = ADXQueryForSpecificTime("assembly", productionLineName, "EnergyConsumption", ((DateTime)timeItWasProducedAssembly["Timestamp"]).ToString("yyyy-MM-dd HH:mm:ss"), idealCycleTime); |
| 60 | + |
| 61 | + // calculate the total energy consumption for the product by summing up all the machines' energy consumptions (in Ws), divide by 3600 to get seconds and multiply by the ideal cycle time (which is in seconds) |
| 62 | + double energyTotal = ((double)energyAssembly["OPCUANodeValue"] + (double)energyTest["OPCUANodeValue"] + (double)energyPackaging["OPCUANodeValue"]) / 3600 * idealCycleTime; |
| 63 | + |
| 64 | + // finally calculate the scope 2 product carbon footprint by multiplying the full energy consumption by the current carbon intensity |
| 65 | + float scope2Emissions = (float)energyTotal * currentCarbonIntensity.data[0].intensity.actual; |
| 66 | + |
| 67 | + // persist in Cloud Library |
| 68 | + PersistInCloudLibrary(productionLineName, serialNumber, scope2Emissions); |
| 69 | + } |
| 70 | + } |
| 71 | + } |
| 72 | + catch (Exception ex) |
| 73 | + { |
| 74 | + Console.WriteLine("CalcPCFForProductionLine: " + ex.Message); |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + private void PersistInCloudLibrary(string productionLineName, double serialNumber, float pcf) |
| 79 | + { |
| 80 | + string dppName = "BatteryPassV6_" + productionLineName + "_" + serialNumber.ToString(); |
| 81 | + |
| 82 | + // write the values to a JSON file |
| 83 | + Dictionary<string, string> values = new() { |
| 84 | + { "i=2", productionLineName + "_" + serialNumber.ToString() }, // UniqueProductIdentifier |
| 85 | + { "i=3", "1.0" }, // DppSchemaVersion |
| 86 | + { "i=4", "Released" }, // DppStatus |
| 87 | + { "i=5", DateTime.UtcNow.ToString() }, // LastUpdate |
| 88 | + { "i=6", "OPC Foundation" }, // EconomicOperatorId |
| 89 | + { "i=11", "GHG Protocol" }, // PCFCalculationMethod |
| 90 | + { "i=12", pcf.ToString() }, // PCFCO2eq |
| 91 | + { "i=13", serialNumber.ToString() }, // PCFReferenceValueForCalculation |
| 92 | + { "i=14", "gCO2" }, // PCFQuantityOfMeasureForCalculation |
| 93 | + { "i=15", "Production & Usage" }, // PCFLifeCyclePhase |
| 94 | + { "i=16", "Scope 2 & 3 Emissions" }, // ExplanatoryStatement |
| 95 | + { "i=21", productionLineName }, // PCFGoodsAddressHandover.CityTown |
| 96 | + { "i=23", DateTime.UtcNow.ToString() } // PublicationDate |
| 97 | + }; |
| 98 | + |
| 99 | + UANameSpace nameSpace = new() { |
| 100 | + Title = dppName, |
| 101 | + License = "MIT", |
| 102 | + CopyrightText = "OPC Foundation", |
| 103 | + Description = "Sample PCF for Digital Twin Consortium production line simulation" |
| 104 | + }; |
| 105 | + nameSpace.Nodeset.NodesetXml = File.ReadAllText("./BatteryPassV6.NodeSet2.xml").Replace("BatteryPassV6", dppName); |
| 106 | + var url = QueryHelpers.AddQueryString( |
| 107 | + _webClient.BaseAddress.AbsoluteUri + "infomodel/upload", |
| 108 | + new Dictionary<string, string> { |
| 109 | + ["overwrite"] = "true", |
| 110 | + ["values"] = JsonConvert.SerializeObject(values) |
| 111 | + } |
| 112 | + ); |
| 113 | + |
| 114 | + HttpResponseMessage response = _webClient.Send(new HttpRequestMessage(HttpMethod.Put, new Uri(url)) { |
| 115 | + Content = new StringContent(JsonConvert.SerializeObject(nameSpace), |
| 116 | + Encoding.UTF8, "application/json") |
| 117 | + }); |
| 118 | + |
| 119 | + if (!response.IsSuccessStatusCode) |
| 120 | + { |
| 121 | + Console.WriteLine("Error uploading BatteryPass to Cloud Library: " + response.StatusCode.ToString()); |
| 122 | + } |
| 123 | + else |
| 124 | + { |
| 125 | + Console.WriteLine("Successfully uploaded BatteryPass to Cloud Library for " + dppName); |
| 126 | + } |
| 127 | + } |
| 128 | + |
| 129 | + private float FindPcf(ErpNode node) |
| 130 | + { |
| 131 | + if (node.events != null) |
| 132 | + { |
| 133 | + foreach (ErpEvent erpEvent in node.events) |
| 134 | + { |
| 135 | + if (erpEvent.productTransactions != null) |
| 136 | + { |
| 137 | + foreach (ErpTransaction transaction in erpEvent.productTransactions) |
| 138 | + { |
| 139 | + if ((transaction.details != null) && (transaction.details.First != null) && ((JProperty)transaction.details.First).Name.ToLowerInvariant() == "pcf") |
| 140 | + { |
| 141 | + return float.Parse(((JProperty)transaction.details.First).Value.ToString()) / transaction.quantity; |
| 142 | + } |
| 143 | + } |
| 144 | + } |
| 145 | + } |
| 146 | + } |
| 147 | + |
| 148 | + if (node.next != null) |
| 149 | + { |
| 150 | + foreach (ErpNode nextNode in node.next) |
| 151 | + { |
| 152 | + float pcf = FindPcf(nextNode); |
| 153 | + if (pcf != 0.0f) |
| 154 | + { |
| 155 | + return pcf; |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + // not found |
| 161 | + return 0.0f; |
| 162 | + } |
| 163 | + |
| 164 | + private Dictionary<string, object> ADXQueryForSpecificValue(string stationName, string productionLineName, string valueToQuery, double desiredValue) |
| 165 | + { |
| 166 | + string query = "opcua_metadata_lkv\r\n" |
| 167 | + + "| where Name contains \"" + stationName + "\"\r\n" |
| 168 | + + "| where Name contains \"" + productionLineName + "\"\r\n" |
| 169 | + + "| join kind = inner(opcua_telemetry\r\n" |
| 170 | + + " | where Name == \"" + valueToQuery + "\"\r\n" |
| 171 | + + " | where Timestamp > now(- 1h)\r\n" |
| 172 | + + ") on DataSetWriterID\r\n" |
| 173 | + + "| distinct Timestamp, OPCUANodeValue = todouble(Value)\r\n" |
| 174 | + + "| sort by Timestamp desc"; |
| 175 | + |
| 176 | + return _adxDataService.RunQuery(query); |
| 177 | + } |
| 178 | + |
| 179 | + private Dictionary<string, object> ADXQueryForSpecificTime(string stationName, string productionLineName, string valueToQuery, string timeToQuery, int idealCycleTime) |
| 180 | + { |
| 181 | + string query = "opcua_metadata_lkv\r\n" |
| 182 | + + "| where Name contains \"" + stationName + "\"\r\n" |
| 183 | + + "| where Name contains \"" + productionLineName + "\"\r\n" |
| 184 | + + "| join kind = inner(opcua_telemetry\r\n" |
| 185 | + + " | where Name == \"" + valueToQuery + "\"\r\n" |
| 186 | + + " | where Timestamp > now(- 1h)\r\n" |
| 187 | + + ") on DataSetWriterID\r\n" |
| 188 | + + "| distinct Timestamp, OPCUANodeValue = todouble(Value)\r\n" |
| 189 | + + "| where around(Timestamp, datetime(" + timeToQuery + "), " + idealCycleTime.ToString() + "s)\r\n" |
| 190 | + + "| sort by Timestamp desc"; |
| 191 | + |
| 192 | + return _adxDataService.RunQuery(query); |
| 193 | + } |
| 194 | + } |
| 195 | +} |
0 commit comments