452 lines
20 KiB
C#
452 lines
20 KiB
C#
using LaaPackages.Features.Cordonel.Models;
|
|
using Newtonsoft.Json;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Text;
|
|
using Xylem.Common.CommonCore.Configuration;
|
|
using Xylem.Common.Hardware.WaterMeter.Genesis.GenesisCore;
|
|
using Xylem.Common.Hardware.WaterMeter.Genesis.Registers;
|
|
using Xylem.Common.Logic.ProductionOrderCore.TestResults;
|
|
using Xylem.Common.Logic.SoftwareAccessHelper;
|
|
|
|
namespace Xylem.Common.Hardware.WaterMeter.Genesis.GenesisStatus
|
|
{
|
|
/// <summary>
|
|
/// Handler for the production status check. This will be feed by the DB.
|
|
/// The base is the standard requirement or the special requirement.
|
|
/// - Calculation of drained battery,
|
|
/// - Assembly capability check,
|
|
/// - Shipping capability check.
|
|
/// </summary>
|
|
public static class GenesisStatusHandler
|
|
{
|
|
/// <summary>
|
|
/// Used to mark an error
|
|
/// </summary>
|
|
public const String ERROR_MARKER = "ERROR";
|
|
|
|
/// <summary>
|
|
/// Average days years
|
|
/// </summary>
|
|
public const Double DAYS_PER_YEAR = 365.25d;
|
|
|
|
/// <summary>
|
|
/// Average days per month based on 365.25 [Days/Year] / 12 [Months/Year] = 30.4375
|
|
/// </summary>
|
|
public const Double DAYS_PER_MONTH = DAYS_PER_YEAR / 12;
|
|
|
|
/// <summary>
|
|
/// Average seconds per month
|
|
/// </summary>
|
|
public const Double SECONDS_PER_MONTH = DAYS_PER_MONTH * 3600 * 24;
|
|
|
|
/// <summary>
|
|
/// Years per seconds
|
|
/// </summary>
|
|
public const Double YEARS_PER_SECONDS = 1.0d / 3600.0 / 24.0 / DAYS_PER_YEAR;
|
|
|
|
/// <summary>
|
|
/// Months from seconds
|
|
/// </summary>
|
|
public const Double MONTHS_PER_SECONDS = 1.0d / SECONDS_PER_MONTH;
|
|
|
|
/// <summary>
|
|
/// Get all production infos out of the "CordonelRequirements" which can be "StandardRequirements" or
|
|
/// "SpecialRequirements". The call of "CordonelRequirements" will automatically select the active
|
|
/// requirement with priority to the "SpecialRequirement".
|
|
/// - Battery drained percentage upper limit,
|
|
/// - Production due date for a special register,
|
|
/// - Required lifetime for assembly line,
|
|
/// - Required lifetime for shipping line.
|
|
/// </summary>
|
|
/// <param name="pcbId"></param>
|
|
/// <param name="prodRequirements"></param>
|
|
/// <param name="errorMsg"></param>
|
|
/// <returns>true if successfully executed</returns>
|
|
/// <remarks date="2023-Mar-02" author="Roland Drabesch/Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
/// <remarks date="2024-Jan-23" author="Roland Drabesch/Thomas Wiedebusch">
|
|
/// - Clone all properties coming from database to reference (deep clone).
|
|
/// </remarks>
|
|
/// <remarks date="2024-Jul-19" author="Thomas Wiedebusch">
|
|
/// - Changed from "ProductionSkipAndLifeTimeInfo" to "CordonelRequirements".
|
|
/// </remarks>
|
|
/// <remarks date="2024-Dec-17" author="Thomas Wiedebusch">
|
|
/// - Overwrite production "ProductionOrderNr" if null with "OuterProductionOrderNr" which is standard for
|
|
/// standard requirements as those are neither PcbId based nor order number based.
|
|
/// </remarks>
|
|
public static Boolean GetCordonelRequirementInfoFromDb(String pcbId, CordonelRequirements prodRequirements,
|
|
out String errorMsg)
|
|
{
|
|
errorMsg = "";
|
|
if (null == prodRequirements || string.IsNullOrEmpty(pcbId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var url = ServiceUrls.GetCordonelRequirementInfoServiceUrl();
|
|
url += pcbId;
|
|
var response = LocalWebRequest.GetRequest(url, 30000, out var httpStatus);
|
|
if (httpStatus == HttpStatusCode.OK && !string.IsNullOrEmpty(response))
|
|
{
|
|
var requirements = JsonConvert.DeserializeObject<CordonelRequirements>(response);
|
|
var type = typeof(CordonelRequirements);
|
|
foreach (var prop in type.GetProperties())
|
|
{
|
|
if (prop.CanWrite)
|
|
prop.SetValue(prodRequirements, prop.GetValue(requirements, null), null);
|
|
}
|
|
|
|
// For standard requirements the order number has to be set as those are neither PcbId based
|
|
// nor order number based
|
|
if (prodRequirements.ProductionOrderNr == null)
|
|
prodRequirements.ProductionOrderNr = prodRequirements.OuterProductionOrderNr;
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
errorMsg = e.Message;
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static Boolean GetCordonelRequirementInfoFromDbByFANr(String fanr, CordonelRequirements prodRequirements, out String errorMsg)
|
|
{
|
|
errorMsg = "";
|
|
if (null == prodRequirements || string.IsNullOrEmpty(fanr))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var url = ServiceUrls.GetCordonelRequirementByFANrInfoServiceUrl(fanr);
|
|
var response = LocalWebRequest.GetRequest(url, 30000, out var httpStatus);
|
|
if (httpStatus == HttpStatusCode.OK && !string.IsNullOrEmpty(response))
|
|
{
|
|
var requirements = JsonConvert.DeserializeObject<CordonelRequirements>(response);
|
|
var type = typeof(CordonelRequirements);
|
|
foreach (var prop in type.GetProperties())
|
|
{
|
|
if (prop.CanWrite)
|
|
prop.SetValue(prodRequirements, prop.GetValue(requirements, null), null);
|
|
}
|
|
|
|
// For standard requirements the order number has to be set as those are neither PcbId based
|
|
// nor order number based
|
|
if (prodRequirements.ProductionOrderNr == null)
|
|
prodRequirements.ProductionOrderNr = prodRequirements.OuterProductionOrderNr;
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
errorMsg = e.Message;
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public static Boolean GetCordonelRequirementInfoFromDbByMeterSize(Int32 size, CordonelRequirements prodRequirements,
|
|
out String errorMsg)
|
|
{
|
|
errorMsg = "";
|
|
|
|
try
|
|
{
|
|
var url = ServiceUrls.GetCordonelRequirementByMeterSizeInfoServiceUrl(size);
|
|
var response = LocalWebRequest.GetRequest(url, 30000, out var httpStatus);
|
|
if (httpStatus == HttpStatusCode.OK && !string.IsNullOrEmpty(response))
|
|
{
|
|
var requirements = JsonConvert.DeserializeObject<CordonelRequirements>(response);
|
|
var type = typeof(CordonelRequirements);
|
|
foreach (var prop in type.GetProperties())
|
|
{
|
|
if (prop.CanWrite)
|
|
prop.SetValue(prodRequirements, prop.GetValue(requirements, null), null);
|
|
}
|
|
|
|
// For standard requirements the order number has to be set as those are neither PcbId based
|
|
// nor order number based
|
|
if (prodRequirements.ProductionOrderNr == null)
|
|
prodRequirements.ProductionOrderNr = prodRequirements.OuterProductionOrderNr;
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
errorMsg = e.Message;
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the calibration results for a meter from a test-bench calibration.
|
|
/// </summary>
|
|
/// <param name="pcbId"></param>
|
|
/// <param name="calibResults"></param>
|
|
/// <param name="errorMsg"></param>
|
|
/// <returns></returns>
|
|
/// <remarks date="2024-11-09" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public static Boolean GetCalibrationResultsFromDb(String pcbId, CalibrationResults calibResults,
|
|
out String errorMsg)
|
|
{
|
|
errorMsg = "";
|
|
if (null == calibResults || string.IsNullOrEmpty(pcbId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var url = ServiceUrls.GetGenesisCalibrationResultsUrl();
|
|
url += pcbId;
|
|
var requestResponse = LocalWebRequest.GetRequest(url, 30000, out var httpStatus);
|
|
if (httpStatus == HttpStatusCode.OK && !string.IsNullOrEmpty(requestResponse))
|
|
{
|
|
var calibRes = JsonConvert.DeserializeObject<CalibrationResults>(requestResponse);
|
|
// ClassAccess.CopyProperties(calibResults, calibRes);
|
|
var type = typeof(CalibrationResults);
|
|
foreach (var prop in type.GetProperties())
|
|
{
|
|
if (prop.CanWrite)
|
|
prop.SetValue(calibResults, prop.GetValue(calibRes, null), null);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
errorMsg = e.Message;
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the state of executed flow tests for a meter from a test-bench run.
|
|
/// </summary>
|
|
/// <param name="pcbId"></param>
|
|
/// <param name="testBenchState"></param>
|
|
/// <param name="errorMsg"></param>
|
|
/// <returns></returns>
|
|
/// <remarks date="2024-11-09" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public static Boolean GetFlowTestStateFromDb(String pcbId, List<String> testBenchState, out String errorMsg)
|
|
{
|
|
errorMsg = "";
|
|
if (testBenchState == null || string.IsNullOrEmpty(pcbId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
try
|
|
{
|
|
var url = ServiceUrls.MeterInfoForTestBench() + pcbId;
|
|
var requestResponse = LocalWebRequest.GetRequest(url, 30000, out var httpStatus);
|
|
if (httpStatus == HttpStatusCode.OK && !string.IsNullOrEmpty(requestResponse))
|
|
{
|
|
var state = JsonConvert.DeserializeObject<String[]>(requestResponse);
|
|
testBenchState.AddRange(state);
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
errorMsg = e.Message;
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read and calculate lifetime information.
|
|
/// </summary>
|
|
/// <param name="currentGenesis"></param>
|
|
/// <param name="status"></param>
|
|
/// <param name="errorMsg"></param>
|
|
/// <returns></returns>
|
|
/// <remarks date="2023-Mar-02" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
/// <remarks date="2024-Nov-15" author="Thomas Wiedebusch">
|
|
/// - errorMsg.
|
|
/// </remarks>
|
|
/// <remarks date="2025-Aug-11" author="Thomas Wiedebusch">
|
|
/// - Readout FW based calculation of remaining lifetime in seconds.
|
|
/// </remarks>
|
|
public static Boolean BuildLifeTimeInformation(IGenesisMeter currentGenesis, GenesisStatus status, out String errorMsg)
|
|
{
|
|
Boolean retVal;
|
|
errorMsg = "";
|
|
if (currentGenesis == null || status == null)
|
|
return false;
|
|
|
|
// read information from genesis
|
|
try
|
|
{
|
|
if (!currentGenesis.IsLoggedOn)
|
|
currentGenesis.Login();
|
|
|
|
status.ExceededLifeTime_s = RegisterConverter.ByteArrayToValue<UInt32>(
|
|
currentGenesis.ReadRegister(Register.Powermon.BatteryExceededSeconds));
|
|
status.DrainedBatteryLoad_uAs = RegisterConverter.ByteArrayToValue<UInt64>(
|
|
currentGenesis.ReadRegister(Register.Powermon.BatteryDrainedLoad, 8));
|
|
status.BatteryQuantity = RegisterConverter.ByteArrayToValue<Int32>(
|
|
currentGenesis.ReadRegister(Register.Powermon.BatteryQuantity));
|
|
status.InitialBatteryLoad_mAh = RegisterConverter.ByteArrayToValue<UInt32>(
|
|
currentGenesis.ReadRegister(Register.Powermon.BatteryInitialLoad));
|
|
|
|
var nullCheck = currentGenesis.ReadRegister(Register.Powermon.RemainingSeconds);
|
|
if (nullCheck == null)
|
|
status.FwCalculatedRemainingLifeTime_s = null;
|
|
else
|
|
status.FwCalculatedRemainingLifeTime_s =
|
|
RegisterConverter.ByteArrayToValue<UInt32>(nullCheck);
|
|
|
|
currentGenesis.Logout();
|
|
|
|
retVal = CalculateLifeTime(status);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
errorMsg = e.Message;
|
|
return false;
|
|
}
|
|
|
|
return retVal;
|
|
}
|
|
/// <summary>
|
|
/// Calculation of lifetime values.
|
|
/// </summary>
|
|
/// <param name="status"></param>
|
|
/// <returns>true if all calculations could be executed</returns>
|
|
/// <remarks date="2023-Mar-02" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
/// <remarks date="2023-Dec-??" author="Roland Drahbesch">
|
|
/// - Uniontown limits UNT.
|
|
/// </remarks>
|
|
/// <remarks date="2024-Sep-25" author="Thomas Wiedebusch">
|
|
/// - Alternative calculation based on quiescent current and production supplement instead of UNT limits.
|
|
/// </remarks>
|
|
/// <remarks date="2024-Oct-22" author="Thomas Wiedebusch">
|
|
/// - Battery assembly time calculation added.
|
|
/// </remarks>
|
|
/// <remarks date="2024-Nov-25" author="Thomas Wiedebusch">
|
|
/// - Exit if essential inputs missing.
|
|
/// </remarks>
|
|
/// <remarks date="2025-Mar-25" author="Thomas Wiedebusch">
|
|
/// - Calculate storage months.
|
|
/// </remarks>
|
|
/// <remarks date="2025-Aug-11" author="Thomas Wiedebusch">
|
|
/// - Calculate FW based calculation of remaining lifetime in years.
|
|
/// </remarks>
|
|
public static Boolean CalculateLifeTime(GenesisStatus status)
|
|
{
|
|
// Mark as false if the basic inputs are missed
|
|
if (status == null ||
|
|
status.InitialBatteryLoad_mAh == 0 ||
|
|
status.BatteryQuantity == 0 ||
|
|
status.DrainedBatteryLoad_uAs == 0 ||
|
|
status.ExceededLifeTime_s == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Calculate the initial overall battery load of all batteries in uAs as the drained load uses this unit.
|
|
var initialBatteryLoad_uAs = 1000.0 * 3600.0 * status.InitialBatteryLoad_mAh * status.BatteryQuantity;
|
|
|
|
// Relation of drained to initial load, set the battery to fully unloaded for failed check to force a recalculation
|
|
var drainedBatteryLoad_rel = status.DrainedBatteryLoad_uAs / initialBatteryLoad_uAs;
|
|
status.DrainedBatteryLoadPercent = drainedBatteryLoad_rel * 100.0;
|
|
|
|
// The estimated lifetime uses the drained load over the actual exceeded lifetime and assumes, that further on
|
|
// an identical load will be consumed over the remaining life cycle.
|
|
var estimatedLifeTime_s = status.ExceededLifeTime_s / drainedBatteryLoad_rel;
|
|
var remainingLifetime_s = estimatedLifeTime_s - status.ExceededLifeTime_s;
|
|
status.RemainingLifeTimeYears = remainingLifetime_s * YEARS_PER_SECONDS;
|
|
status.FwCalculatedRemainingLifeTimeYears = status.FwCalculatedRemainingLifeTime_s * YEARS_PER_SECONDS;
|
|
|
|
// Calculate an alternative threshold for remaining battery load based on the quiescent current and a supplement
|
|
// for production purpose. Those values can be assigned in the Cordonel requirements.
|
|
if (status.QuiescentCurrent_uA != null &&
|
|
status.QuiescentCurrent_uA > 0 &&
|
|
status.EstimatedProductionBatteryLoadPercent != null)
|
|
{
|
|
status.AlternativeRequiredBatteryLoadPercent =
|
|
// The additional free power for production purposes
|
|
(Double)status.EstimatedProductionBatteryLoadPercent +
|
|
// The power consumption based on the actual lifetime and consumed current in relation to absolute load
|
|
status.ExceededLifeTime_s * (Double)status.QuiescentCurrent_uA / initialBatteryLoad_uAs * 100.0;
|
|
}
|
|
|
|
// Calculate battery assembly time
|
|
var actualDate = DateTime.UtcNow;
|
|
status.BatteryAssemblyDateTimeUtc = actualDate.AddSeconds(-(Double)status.ExceededLifeTime_s);
|
|
var timeDifference = actualDate - status.BatteryAssemblyDateTimeUtc;
|
|
// DaysPerMonth = 365.25 [Days/Year] / 12 [Months/Year] = 30.4375
|
|
status.StorageMonths = timeDifference.GetValueOrDefault().TotalSeconds * MONTHS_PER_SECONDS;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// String of lifetime calculation results.
|
|
/// </summary>
|
|
/// <param name="currentGenesis"></param>
|
|
/// <param name="genesisStatus"></param>
|
|
/// <returns>true if all calculations could be executed</returns>
|
|
/// <remarks date="2023-Mar-02" author="Thomas Wiedebusch">
|
|
/// - Initial.
|
|
/// </remarks>
|
|
public static String LifeTimeInformationString(IGenesisMeter currentGenesis,GenesisStatus genesisStatus)
|
|
{
|
|
var sbMSG = new StringBuilder();
|
|
if (genesisStatus == null ||
|
|
currentGenesis == null ||
|
|
!BuildLifeTimeInformation(currentGenesis, genesisStatus, out _))
|
|
{
|
|
sbMSG.AppendLine($"{ERROR_MARKER}: Unable to calculate storage time and battery power consumption!");
|
|
return sbMSG.ToString();
|
|
}
|
|
|
|
sbMSG.AppendLine($"POWERMON_TotalUsedSeconds: {genesisStatus.ExceededLifeTime_s:N0} s");
|
|
sbMSG.AppendLine($"POWERMON_TotalUsedCharge: {genesisStatus.DrainedBatteryLoad_uAs:N0} µAs");
|
|
sbMSG.AppendLine($"POWERMON_BatteryQuantity: {genesisStatus.BatteryQuantity} pieces");
|
|
sbMSG.AppendLine($"POWERMON_BatteryMilliAHrRating: {genesisStatus.InitialBatteryLoad_mAh:N0} mAh");
|
|
|
|
sbMSG.AppendLine(genesisStatus.FwCalculatedRemainingLifeTimeYears != null ?
|
|
$"POWERMON_RemainingSeconds: {genesisStatus.FwCalculatedRemainingLifeTime_s:N0} s" :
|
|
"POWERMON_RemainingSeconds: unsupported");
|
|
|
|
sbMSG.AppendLine("");
|
|
sbMSG.AppendLine($"Actual storage time: {genesisStatus.StorageMonths:F1} months");
|
|
sbMSG.AppendLine("SW estimated remaining lifetime based on average power consumption: " +
|
|
$"{genesisStatus.RemainingLifeTimeYears:F2} years");
|
|
sbMSG.AppendLine(genesisStatus.FwCalculatedRemainingLifeTimeYears != null
|
|
? "FW calculated remaining lifetime based on 'remaining seconds': " +
|
|
$"{genesisStatus.FwCalculatedRemainingLifeTimeYears:F2} years"
|
|
: "FW calculated remaining lifetime based on 'remaining seconds' unsupported by FW: " +
|
|
$"{currentGenesis.FwVersion}");
|
|
sbMSG.AppendLine($"Totally drained battery load: {genesisStatus.DrainedBatteryLoadPercent:F2} %");
|
|
|
|
return sbMSG.ToString();
|
|
}
|
|
}
|
|
}
|