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 { /// /// 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. /// public static class GenesisStatusHandler { /// /// Used to mark an error /// public const String ERROR_MARKER = "ERROR"; /// /// Average days years /// public const Double DAYS_PER_YEAR = 365.25d; /// /// Average days per month based on 365.25 [Days/Year] / 12 [Months/Year] = 30.4375 /// public const Double DAYS_PER_MONTH = DAYS_PER_YEAR / 12; /// /// Average seconds per month /// public const Double SECONDS_PER_MONTH = DAYS_PER_MONTH * 3600 * 24; /// /// Years per seconds /// public const Double YEARS_PER_SECONDS = 1.0d / 3600.0 / 24.0 / DAYS_PER_YEAR; /// /// Months from seconds /// public const Double MONTHS_PER_SECONDS = 1.0d / SECONDS_PER_MONTH; /// /// 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. /// /// /// /// /// true if successfully executed /// /// - Initial. /// /// /// - Clone all properties coming from database to reference (deep clone). /// /// /// - Changed from "ProductionSkipAndLifeTimeInfo" to "CordonelRequirements". /// /// /// - Overwrite production "ProductionOrderNr" if null with "OuterProductionOrderNr" which is standard for /// standard requirements as those are neither PcbId based nor order number based. /// 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(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(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(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; } /// /// Get the calibration results for a meter from a test-bench calibration. /// /// /// /// /// /// /// - Initial. /// 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(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; } /// /// Get the state of executed flow tests for a meter from a test-bench run. /// /// /// /// /// /// /// - Initial. /// public static Boolean GetFlowTestStateFromDb(String pcbId, List 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(requestResponse); testBenchState.AddRange(state); return true; } } catch (Exception e) { errorMsg = e.Message; return false; } return false; } /// /// Read and calculate lifetime information. /// /// /// /// /// /// /// - Initial. /// /// /// - errorMsg. /// /// /// - Readout FW based calculation of remaining lifetime in seconds. /// 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( currentGenesis.ReadRegister(Register.Powermon.BatteryExceededSeconds)); status.DrainedBatteryLoad_uAs = RegisterConverter.ByteArrayToValue( currentGenesis.ReadRegister(Register.Powermon.BatteryDrainedLoad, 8)); status.BatteryQuantity = RegisterConverter.ByteArrayToValue( currentGenesis.ReadRegister(Register.Powermon.BatteryQuantity)); status.InitialBatteryLoad_mAh = RegisterConverter.ByteArrayToValue( 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(nullCheck); currentGenesis.Logout(); retVal = CalculateLifeTime(status); } catch (Exception e) { errorMsg = e.Message; return false; } return retVal; } /// /// Calculation of lifetime values. /// /// /// true if all calculations could be executed /// /// - Initial. /// /// /// - Uniontown limits UNT. /// /// /// - Alternative calculation based on quiescent current and production supplement instead of UNT limits. /// /// /// - Battery assembly time calculation added. /// /// /// - Exit if essential inputs missing. /// /// /// - Calculate storage months. /// /// /// - Calculate FW based calculation of remaining lifetime in years. /// 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; } /// /// String of lifetime calculation results. /// /// /// /// true if all calculations could be executed /// /// - Initial. /// 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(); } } }