common/Hardware/WaterMeter/Genesis/GenesisStatus/GenesisStatusHandler.cs
2026-04-23 17:50:07 +02:00

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();
}
}
}