using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Configuration; using System.IO.Ports; using System.Linq; using System.Text; using System.Threading; using Timer = System.Timers.Timer; using System.Collections; using HengShan_Pump_NonIC.MessageEntity; using Edge.Core.Processor;using Edge.Core.IndustryStandardInterface.Pump; using Wayne.FDCPOSLibrary; using System.Xml; using Edge.Core.Database.Models; using System.Threading.Tasks; namespace HengShan_Pump_NonIC { public class PumpHandler : IFdcPumpController, IDeviceHandler { //static ILog fdcLogger = log4net.LogManager.GetLogger("FdcServer"); //static ILog logger = log4net.LogManager.GetLogger("PumpHandler"); static NLog.Logger logger = NLog.LogManager.LoadConfiguration("nlog.config").GetLogger("PumpHandler"); protected IContext context; public event EventHandler OnStateChange; /// /// fired on fueling process is on going, the fuel amount should keep changing. /// public event EventHandler OnCurrentFuellingStatusChange; protected LogicalDeviceState lastLogicalDeviceState = LogicalDeviceState.FDC_CLOSED; private DateTime lastLogicalDeviceStateReceivedTime; // by seconds private const int lastLogicalDeviceStateExpiredTime = 6; /// /// Indicator for OnFdcServiceInit function called, the Process() will be called eariler that this function, /// protected bool isOnFdcServerInitCalled = false; private Guid uniqueId = Guid.NewGuid(); private int pumpId = -1; protected List nozzles = new List(); /// /// this type of pump state change is detected by FC actively polling, then state is always delay reported, so there's a corner case that in a fueling process, /// the attendants put back and pull out nozzle very quickly and it happened exactly in the middle of a polling, meanwhile, /// an auth request was done to auth the pump again(most likely the autoAuthCallingPump set with True), /// then the pump state returned from physical pump will still be read as a fueling state, but actually the /// 2nd fueling process is started, so detect the pump state is not enough, need detect if the fueling seq number reset to null(if place back nozzle detected) or not. /// protected GetNozzleStatusResponse previousUnfinishedFuelingNozzleStatus; public IEnumerable Nozzles => this.nozzles; protected void FireOnStateChangeEvent(LogicalDeviceState state) { var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(state, this.nozzles.First())); } protected void FireOnCurrentFuellingStatusChangeEvent(FdcTransaction trx) { var safe = this.OnCurrentFuellingStatusChange; safe?.Invoke(this, new FdcTransactionDoneEventArg(trx)); } /// /// 恒山非IC油机 /// /// /// public PumpHandler(int pumpId, string nozzlesXmlConfiguration) { this.pumpId = pumpId; // real nozzle Id put hardcode 1 here since HengShan_pump_nonIC only have 1 nozzle per pump. // price is not dectected yet, put Null. this.nozzles.Add(new LogicalNozzle(pumpId, 1, 1, null)); } //protected bool isSafeForSend = false; public void Init(IContext context) { this.context = context; //this.polling = new Timer(this.pollingInterval); //this.polling.Elapsed += (_, __) => //{ // lock (this.syncObject) // { // this.isSafeForSend = false; // context.Outgoing.Write(new GetNozzleStatusRequest()); // } //}; //this.polling.Start(); var timeWindowWithActivePollingOutgoing = this.context.Outgoing as TimeWindowWithActivePollingOutgoing; timeWindowWithActivePollingOutgoing.PollingMsgProducer = () => new GetNozzleStatusRequest(); } public virtual async Task Process(IContext context) { if (!isOnFdcServerInitCalled) return; this.context = context; if (context.Incoming.Message is GetNozzleStatusResponse getNozzleStatusResponse) { //this.isSafeForSend = true; this.lastLogicalDeviceStateReceivedTime = DateTime.Now; // put the price by reading the real price. this.nozzles.First().RealPriceOnPhysicalPump = getNozzleStatusResponse.单价; var latestStatus = getNozzleStatusResponse.GetPumpStatus(); if (latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.油枪打开) && latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.加油结束) && latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.不允许加油) && latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.电机关) && !latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.加油过程)) { /* 非加油状态下的提枪 */ if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_AUTHORISED) { logger.Info("Pump: " + this.pumpId + ", " + "收到新状态: 提枪"); //说明是未提枪时就已经auth成功了,所以还是保持发送 FDC_AUTHORISED 的状态给FdcClient。 logger.Debug("Pump: " + this.pumpId + ", " + " 未提枪时就已经auth成功了, do nothing"); } else { if (this.previousUnfinishedFuelingNozzleStatus != null) { this.previousUnfinishedFuelingNozzleStatus = null; // no data loss since getNozzleStatusResponse will still carry last trx data until an auth. logger.Info("Pump: " + this.pumpId + ", " + "Detected a fast put back and pull out nozzle case(action time < polling time cause nozzle place back not detected)," + "\r\n will fire the trx done event for earlier trx(no data loss)->\r\n " + "seqNo: " + getNozzleStatusResponse.流水号 + ", amount: " + getNozzleStatusResponse.加油金额 + ", volume: " + getNozzleStatusResponse.加油量 + ", price: " + getNozzleStatusResponse.单价); logger.Info("Pump: " + this.pumpId + ", " + " State switched to FDC_READY(simulate)"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_READY; this.FireOnStateChangeEvent(LogicalDeviceState.FDC_READY); var previousFuelingAmount = getNozzleStatusResponse.加油金额; var previousFuelingVol = getNozzleStatusResponse.加油量; var previousPrice = getNozzleStatusResponse.单价; var previousSeqNo = getNozzleStatusResponse.流水号; if (this.logicalNozzleIdToLastFuelSaleTrxMapping.ContainsKey(1)) this.logicalNozzleIdToLastFuelSaleTrxMapping[1] = new FuelSaleTransaction() { TransactionSeqNumberFromPhysicalPump = getNozzleStatusResponse.流水号.ToString() }; else this.logicalNozzleIdToLastFuelSaleTrxMapping.Add(1, new FuelSaleTransaction() { TransactionSeqNumberFromPhysicalPump = getNozzleStatusResponse.流水号.ToString() }); //ThreadPool.QueueUserWorkItem(o => //{ var totalizer = await this.QueryTotalizerAsync(1); this.FireOnCurrentFuellingStatusChangeEvent(new FdcTransaction() { // 恒山油机只有一把枪 Nozzle = this.nozzles.First(), Amount = previousFuelingAmount, Volumn = previousFuelingVol, Price = previousPrice, SequenceNumberGeneratedOnPhysicalPump = previousSeqNo, AmountTotalizer = totalizer.Item1, VolumeTotalizer = totalizer.Item2, Finished = true, }); //}); } if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_CALLING) { logger.Info("Pump: " + this.pumpId + ", " + "收到新状态: 提枪"); logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_CALLING"); //直接提枪了 this.lastLogicalDeviceState = LogicalDeviceState.FDC_CALLING; var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_CALLING, this.nozzles.First())); } } } else if (latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.允许加油) && latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.油枪打开) && (latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.加油过程) || latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.电机打开))) { /* 正处于加油状态下 */ this.previousUnfinishedFuelingNozzleStatus = getNozzleStatusResponse; if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_FUELLING) { //status code: B1 logger.Info("Pump: " + this.pumpId + ", " + "收到新状态: 加油状态中"); logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_FUELLING"); this.lastLogicalDeviceState = LogicalDeviceState.FDC_FUELLING; var safe0 = this.OnStateChange; safe0?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_FUELLING, this.nozzles.First())); } logger.Debug("Pump: " + this.pumpId + ", " + " fueling in progress with amt: " + getNozzleStatusResponse.加油量 + ", vol: " + getNozzleStatusResponse.加油量 + ", seq: " + getNozzleStatusResponse.流水号); //fire fuelling progress. var safe1 = this.OnCurrentFuellingStatusChange; safe1?.Invoke(this, new FdcTransactionDoneEventArg(new FdcTransaction() { // 恒山油机只有一把枪 Nozzle = this.nozzles.First(), Amount = getNozzleStatusResponse.加油金额, Volumn = getNozzleStatusResponse.加油量, Price = getNozzleStatusResponse.单价, SequenceNumberGeneratedOnPhysicalPump = getNozzleStatusResponse.流水号, Finished = false, })); } else if (latestStatus.Any(f => f == GetNozzleStatusResponse.PumpStatus.加油结束)) { /* 油机首次上电也会进入此处,并不断发送上一次的加油记录,这种情况下的交易记录并无法判断其之前是否已经发送至系统(并存入数据库), 所以继续往系统里送入,由系统判断重复情况。 而其它正常加油过程中的交易记录仅在油机状态变化时才送入系统。*/ // status code: 40 //logger.Debug("Pump: " + this.pumpId + ", " + "收到状态: 加油结束"); this.previousUnfinishedFuelingNozzleStatus = null; if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_READY) { logger.Debug("Pump: " + this.pumpId + ", " + "收到新状态: 加油结束, will send StopRequest(Unauth) anyway"); this.context.Outgoing.WriteAsync(new StopRequest(), (request, testResponse) => testResponse is StopResponse, (request, response) => { if (response == null) logger.Info("Pump: " + this.pumpId + ", " + "StopRequest(Unauth) timed out"); else { var stopResponse = (StopResponse)response; if (stopResponse.EnumResult == NonICMessageTemplateBase.Result.失败) logger.Info("Pump: " + this.pumpId + ", " + "StopRequest(Unauth) respond a Failure"); } }, 2000); logger.Debug("Pump: " + this.pumpId + ", " + " State switched to FDC_READY"); lastLogicalDeviceState = LogicalDeviceState.FDC_READY; var safe0 = this.OnStateChange; safe0?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_READY, null)); // zero trx, do nothing. if (getNozzleStatusResponse.加油量 == 0 || getNozzleStatusResponse.加油金额 == 0) return; // repeat received last fuel sale message, do nothing. if (this.logicalNozzleIdToLastFuelSaleTrxMapping.ContainsKey(1) && this.logicalNozzleIdToLastFuelSaleTrxMapping[1].TransactionSeqNumberFromPhysicalPump == getNozzleStatusResponse.流水号.ToString()) return; // lastSale exists and 流水号 diff if (this.logicalNozzleIdToLastFuelSaleTrxMapping.ContainsKey(1) && this.logicalNozzleIdToLastFuelSaleTrxMapping[1].TransactionSeqNumberFromPhysicalPump != getNozzleStatusResponse.流水号.ToString()) { logger.Info("Pump: " + this.pumpId + ", Detect a fuel trx with 流水号: " + getNozzleStatusResponse.流水号 + " which is diff from previous fuel trx(previous 流水号:" + this.logicalNozzleIdToLastFuelSaleTrxMapping[1].TransactionSeqNumberFromPhysicalPump + "), will generate a new fuel sale trx with vol: " + getNozzleStatusResponse.加油量); this.logicalNozzleIdToLastFuelSaleTrxMapping[1] = new FuelSaleTransaction() { TransactionSeqNumberFromPhysicalPump = getNozzleStatusResponse.流水号.ToString() }; } // lastSale not exists if (!this.logicalNozzleIdToLastFuelSaleTrxMapping.ContainsKey(1)) { logger.Info("Pump: " + this.pumpId + ", very first initial trx detected on this pump, will generate a new fuel sale trx(seq: " + getNozzleStatusResponse.流水号 + ", vol: " + getNozzleStatusResponse.加油量 + ", amt: " + getNozzleStatusResponse.加油金额 + ")"); this.logicalNozzleIdToLastFuelSaleTrxMapping.Add(1, new FuelSaleTransaction() { TransactionSeqNumberFromPhysicalPump = getNozzleStatusResponse.流水号.ToString() }); } var amount = getNozzleStatusResponse.加油金额; var fuelingVol = getNozzleStatusResponse.加油量; var price = getNozzleStatusResponse.单价; var seqNo = getNozzleStatusResponse.流水号; //ThreadPool.QueueUserWorkItem(o => //{ var totalizer = await this.QueryTotalizerAsync(1); logger.Info("Pump: " + this.pumpId + ", fire trx done with amt: " + amount + ", vol: " + fuelingVol + ", price: " + price + ", seqNo.: " + seqNo); var safe1 = this.OnCurrentFuellingStatusChange; safe1?.Invoke(this, new FdcTransactionDoneEventArg(new FdcTransaction() { // 恒山油机 一个加油点只有一把枪 Nozzle = this.nozzles.First(), Amount = amount, Volumn = fuelingVol, Price = price, SequenceNumberGeneratedOnPhysicalPump = seqNo, AmountTotalizer = totalizer.Item1, VolumeTotalizer = totalizer.Item2, Finished = true, })); //}); } } else { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_ERRORSTATE) { this.lastLogicalDeviceState = LogicalDeviceState.FDC_ERRORSTATE; logger.Debug("Pump: " + this.pumpId + ", " + "收到未知状态: " + getNozzleStatusResponse.ToLogString() + ", \r\n switch to FDC_ERRORSTATE"); } } } } public string Name => this.GetType().FullName; public Guid Id => this.uniqueId; /// /// Gets the Identification of the pump for the system. Is the logical number of the pump /// public int PumpId => this.pumpId; /// /// this pump have no way to share same comport since this HengShan protocol content does not contains /// any id info, so always static 0 here. /// 地址面地址 /// public int PumpPhysicalId => 0; public int AmountDecimalDigits => 2; public int VolumeDecimalDigits => 2; public int PriceDecimalDigits => 2; public int VolumeTotalizerDecimalDigits => 2; public virtual async Task QueryStatusAsync() { // if last state is expired, we return a OFFLINE here to FdcClient. if (DateTime.Now.Subtract(this.lastLogicalDeviceStateReceivedTime).TotalSeconds > lastLogicalDeviceStateExpiredTime) { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_OFFLINE) { this.lastLogicalDeviceState = LogicalDeviceState.FDC_OFFLINE; logger.Info("Pump: " + this.pumpId + ", " + " State switched to FDC_OFFLINE due to cached state expired"); var safe0 = this.OnStateChange; safe0?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_OFFLINE, null)); } return LogicalDeviceState.FDC_OFFLINE; } return this.lastLogicalDeviceState; } /// /// /// /// MoneyTotalizer:VolumnTotalizer public async Task> QueryTotalizerAsync(byte logicalNozzleId) { logger.Info("Pump: " + this.pumpId + ", " + " Start QueryTotalizer for logicalNozzle: " + logicalNozzleId); if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_CLOSED || this.lastLogicalDeviceState == LogicalDeviceState.FDC_OFFLINE) { logger.Info("Pump: " + this.pumpId + ", " + " Pump is in state FDC_CLOSED or FDC_OFFLINE, will return -1, -1"); return new System.Tuple(-1, -1); } var result = new System.Tuple(-1, -1); var response = await this.context.Outgoing.WriteAsync(new GetAccumulateRequest(), (request, testResponse) => testResponse is GetAccumulateResponse, 2000); if (response == null) logger.Info("Pump: " + this.pumpId + ", " + "QueryTotalizer timed out"); else { var accumResponse = (GetAccumulateResponse)response; result = new Tuple(accumResponse.金额累计, accumResponse.升累计); } return result; } public virtual async Task ChangeFuelPriceAsync(int newPriceWithoutDecimalPoint, byte logicalNozzleId) { logger.Info("Pump: " + this.pumpId + ", " + " Start ChangeFuelPrice for logicalNozzle: " + logicalNozzleId + " with new price(without decimalPoints): " + newPriceWithoutDecimalPoint); if (this.lastLogicalDeviceState == LogicalDeviceState.FDC_CLOSED || this.lastLogicalDeviceState == LogicalDeviceState.FDC_OFFLINE) { logger.Info("Pump: " + this.pumpId + ", " + " Pump is in state FDC_CLOSED or FDC_OFFLINE, ChangeFuelPrice will return false"); return false; } var succeed = false; var response = await this.context.Outgoing.WriteAsync(new SetFuelPriceRequest() { FuelPrice = newPriceWithoutDecimalPoint }, (request, testResponse) => testResponse is SetFuelPriceResponse, 2500); if (response == null) logger.Info("Pump: " + this.pumpId + ", " + "ChangeFuelPrice timed out"); else { var priceChangeResponse = (SetFuelPriceResponse)response; if (priceChangeResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("Pump: " + this.pumpId + ", " + "ChangeFuelPriceResponse is NOT NonICMessageTemplateBase.Result.成功"); succeed = false; } else { logger.Info("Pump: " + this.pumpId + ", " + "ChangeFuelPriceResponse succeed"); succeed = true; } } return succeed; } /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual async Task AuthorizeAsync(byte logicalNozzleId) { logger.Info("Pump: " + this.pumpId + ", " + "Start Authorize for logicalNozzle: " + logicalNozzleId); var succeed = false; var response = await this.context.Outgoing.WriteAsync(new StartRequest(), (request, testResponse) => testResponse is StartResponse, 2500); if (response == null) { logger.Info("Pump: " + this.pumpId + ", " + "Authorize timed out"); } else { var startResponse = (StartResponse)response; if (startResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("Pump: " + this.pumpId + ", " + "StartResponse is NOT NonICMessageTemplateBase.Result.成功"); succeed = false; } else { logger.Info("Pump: " + this.pumpId + ", " + "Authorize (StartResponse) succeed"); succeed = true; } } return succeed; } /// /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual async Task AuthorizeWithAmountAsync(int moneyAmountWithoutDecimalPoint, byte logicalNozzleId) { logger.Info("Pump: " + this.pumpId + ", " + "Start AuthorizeWithAmount for logicalNozzle: " + logicalNozzleId + " with money(without decimalPoint): " + moneyAmountWithoutDecimalPoint); var succeed = false; var response = await this.context.Outgoing.WriteAsync(new AuthPumpWithAmountRequest() { Amount = moneyAmountWithoutDecimalPoint }, (request, testResponse) => testResponse is AuthPumpWithAmountResponse, 4000); if (response == null) logger.Info("Pump: " + this.pumpId + ", " + "AuthorizeWithAmount timed out"); var presetResponse = (AuthPumpWithAmountResponse)response; if (presetResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("Pump: " + this.pumpId + ", " + "AuthPumpWithAmountResponse is NOT NonICMessageTemplateBase.Result.成功"); succeed = false; } else { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_AUTHORISED) { lastLogicalDeviceState = LogicalDeviceState.FDC_AUTHORISED; var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_AUTHORISED, this.nozzles.First())); } Thread.Sleep(500); succeed = await this.AuthorizeAsync(logicalNozzleId); } return succeed; } /// /// /// /// /// useless for this type of pump, it always one pump one nozzle /// public virtual async Task AuthorizeWithVolumeAsync(int volumnWithoutDecimalPoint, byte logicalNozzleId) { logger.Info("Pump: " + this.pumpId + ", " + "Start AuthorizeWithVolumn for logicalNozzle: " + logicalNozzleId + " with vol(without decimalPoint): " + volumnWithoutDecimalPoint); var succeed = false; var response = await this.context.Outgoing.WriteAsync(new AuthPumpWithGallonRequest() { Gallon = volumnWithoutDecimalPoint }, (request, testResponse) => testResponse is AuthPumpWithGallonResponse, 4000); if (response == null) logger.Info("Pump: " + this.pumpId + ", " + "AuthPumpWithGallonRequest timed out"); var presetResponse = (AuthPumpWithGallonResponse)response; if (presetResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("Pump: " + this.pumpId + ", " + "AuthorizeWithVolumnResponse is NOT NonICMessageTemplateBase.Result.成功"); succeed = false; } else { if (this.lastLogicalDeviceState != LogicalDeviceState.FDC_AUTHORISED) { lastLogicalDeviceState = LogicalDeviceState.FDC_AUTHORISED; var safe = this.OnStateChange; safe?.Invoke(this, new FdcPumpControllerOnStateChangeEventArg(LogicalDeviceState.FDC_AUTHORISED, this.nozzles.First())); } Thread.Sleep(500); succeed = await this.AuthorizeAsync(logicalNozzleId); } return succeed; } public virtual async Task FuelingRoundUpByAmountAsync(int amount) { logger.Info("Pump: " + this.pumpId + ", " + " Start FuelingRoundUpByAmount, amount: " + amount + " will be ignored due to hardware limit"); var succeed = false; var response = await this.context.Outgoing.WriteAsync(new RoundUpByAmountRequest(), (request, testResponse) => testResponse is RoundUpByAmountResponse, 2500); if (response == null) { succeed = false; logger.Info("Pump: " + this.pumpId + ", " + "FuelingRoundUpByAmount timed out"); } else { var roundUpResponse = (RoundUpByAmountResponse)response; if (roundUpResponse.EnumResult != NonICMessageTemplateBase.Result.成功) { logger.Info("Pump: " + this.pumpId + ", " + "roundUpResponse is NOT NonICMessageTemplateBase.Result.成功"); succeed = false; } else succeed = true; } return succeed; } #region not implemented public async Task UnAuthorizeAsync(byte logicalNozzleId) { throw new NotImplementedException(); } public async Task SuspendFuellingAsync() { throw new NotImplementedException(); } public async Task ResumeFuellingAsync() { throw new NotImplementedException(); } public async Task FuelingRoundUpByVolumeAsync(int volume) { throw new NotImplementedException(); } #endregion /// /// protected Dictionary logicalNozzleIdToLastFuelSaleTrxMapping = new Dictionary(); public void OnFdcServerInit(Dictionary parameters) { if (parameters.ContainsKey("LastPriceChange")) { } /* Load Last sale(from db) for void the case of FC accidently disconnect from Pump in fueling, and may cause a fueling trx gone from FC control */ if (parameters.ContainsKey("LastFuelSaleTrx")) { // nozzle logical id:lastSale var lastFuelSaleTrxes = parameters["LastFuelSaleTrx"] as Dictionary; foreach (var lastFuelSaleTrx in lastFuelSaleTrxes) { logger.Info("Pump: " + this.pumpId + ", OnFdcServerInit, load last fuel sale " + "on logical nozzle: " + lastFuelSaleTrx.Key + " with value: " + lastFuelSaleTrx.Value); this.logicalNozzleIdToLastFuelSaleTrxMapping.Remove(lastFuelSaleTrx.Key); this.logicalNozzleIdToLastFuelSaleTrxMapping.Add(lastFuelSaleTrx.Key, lastFuelSaleTrx.Value); } } this.isOnFdcServerInitCalled = true; } public async Task LockNozzleAsync(byte logicalNozzleId) { return false; } public async Task UnlockNozzleAsync(byte logicalNozzleId) { return false; } } }