Skip to content

Commit 108043b

Browse files
committed
Added BatteryPass skelleton.
1 parent 31561f6 commit 108043b

File tree

5 files changed

+2231
-12
lines changed

5 files changed

+2231
-12
lines changed

BatteryPassProcessor.cs

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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

Comments
 (0)