-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathcar
executable file
·790 lines (479 loc) · 26.5 KB
/
car
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
#!/usr/bin/env python3
###############################################################################
###############################################################################
# Copyright (c) 2024, Andy Schroder
# See the file README.md for licensing information.
###############################################################################
###############################################################################
################################################################
# import modules
################################################################
# dc must be the first module initialized and then immediately set the mode
import dc
dc.mode='car'
# dc.common must be the second imported module because it reads the config
from dc.common import ConfigFile, m3, getCANvalue, can_Message, SWCAN_Relay, SWCAN, SWCAN_ISOTP, TWCAN, lnd, GUI, logger, LogData
from time import sleep,time
from datetime import datetime,timedelta
from helpers2 import FormatTimeDeltaToPaddedString,RoundAndPadToString,SetPrintWarningMessages
from textwrap import indent,TextWrapper
import sys
from collections import deque
from threading import Thread, Event
from bolt11.core import decode
from math import ceil
################################################################
#define configuration related constants
################################################################
MaxRate=1.5 #sat/(W*hour)
MaxRequiredPaymentAmount=41 #sat
MaxFeeFraction=0.03
MaxFeeSat=2
################################################################
################################################################
# initialize variables
################################################################
SWCANActive=False
SWCANConnected=False
Proximity=False
AcceptedRate=False
Power=0
ChargeStartTime=-1
CurrentRate=0
RequiredPaymentAmountAccepted=0
EnergyDelivered=0
EnergyPaidFor=0
NumberOfPaymentsReceived=0
BigStatus='Insert Charge Cable Into Car'
SmallStatus='Waiting For Charge Cable To Be Inserted'
################################################################
################################################################
# define functions and classes
################################################################
class DocumentWrapper(TextWrapper):
# inspired from https://stackoverflow.com/questions/1166317/python-textwrap-library-how-to-preserve-line-breaks/45287550#45287550
# keep original newlines and only wrap lines that are longer than the limit
def wrap(self, text):
split_text = text.split('\n')
lines = [line for para in split_text for line in TextWrapper.wrap(self, para)]
return lines
class ReceiveInvoices(Thread):
#this class holds the InvoiceQueue object, receives invoices, operates in another thread in daemon mode, and will shutdown if the .stop() method is used.
#not sure if the socket should be opened and closed from within here or not. to be re-visited at a later time.
#not sure if socket needs to be re-created every time SWCAN comes up.
#see also:
# https://stackoverflow.com/questions/47912701/python-how-can-i-implement-a-stoppable-thread
# https://stackoverflow.com/questions/40382332/example-usages-of-the-stoppablethread-subclass-of-pythons-threading-thread
# https://github.com/python/cpython/blob/2.7/Lib/threading.py#L743
# https://stackoverflow.com/questions/27102881/python-threading-self-stop-event-object-is-not-callable
def __init__(self, *args, **kwargs):
super(ReceiveInvoices, self).__init__(*args, **kwargs)
self._stop_thread = Event()
self.InvoiceQueue=deque()
self.daemon=True # using daemon mode so control-C will stop the script and the threads.
logger.info('listening on SWCAN for new invoices')
self.start() # auto start on initialization
def stop(self):
logger.debug('ReceiveInvoices thread stop requested')
self._stop_thread.set()
def stopped(self):
return self._stop_thread.is_set()
def run(self):
while True:
try:
NewInvoice=SWCAN_ISOTP.recv() #SWCAN_ISOTP is set to timeout every 0.1 seconds, so it automatically sleeps for us
except:
logger.exception('error with SWCAN_ISOTP.recv')
sleep(5)
else:
if NewInvoice is not None:
self.InvoiceQueue.append(NewInvoice.decode()) # SWCAN_ISOTP receives data as binary, so need to run .decode() to convert it back to a string.
logger.info('new invoice received and added to the queue. total outstanding invoices is now '+str(len(self.InvoiceQueue)))
if self._stop_thread.is_set():
break
logger.info('stopped ReceiveInvoices thread')
class SWCANMessagesClass(Thread):
def __init__(self, GUI=None):
super(SWCANMessagesClass, self).__init__()
self._stop_thread = Event()
self.message=None
self.WhoursPerPayment=None
self.RequiredPaymentAmount=None
self.daemon=True # using daemon mode so control-C will stop the script and the threads and .join() can timeout and if the main thread crashes, then it will all crash and restart automatically (by systemd).
logger.info('initialized SWCANMessagesClass thread')
self.start() # auto start on initialization
def stop(self):
logger.debug('SWCANMessagesClass thread stop requested')
self._stop_thread.set()
def run(self):
while True:
try:
# according to https://github.com/hardbyte/python-can/issues/768 there is some kind of buffer. not sure what it actually is
# but since the frequency of all messages of interest is low after applying the filter, hoping it is good enough for now.
self.message = SWCAN.recv(timeout=0.5) # listen for messages even if not Proxmity because if there is no proximity, there will just be nothing there.
except:
logger.exception('error with SWCAN.recv')
sleep(5)
else:
if (self.message is not None):
if (self.message.arbitration_id == 1998): #offer received
self.WhoursPerPayment=int.from_bytes(self.message.data[0:4],byteorder='little') #Whours_offered
self.RequiredPaymentAmount=int.from_bytes(self.message.data[4:8],byteorder='little') #for_sat
if self._stop_thread.is_set():
break
logger.info('stopped SWCANMessagesClass thread')
class TWCANMessagesClass(Thread):
def __init__(self, GUI=None):
super(TWCANMessagesClass, self).__init__()
self._stop_thread = Event()
self.message=None
self.TESLA_SWCAN_ESTABLISHED=False
self.AC_CHARGE_ENABLED=False
self.TotalWhoursCharged=-1
self.Volts=None
self.MaxAmps=0
self.Amps=None
self.StateOfCharge=0
self.daemon=True # using daemon mode so control-C will stop the script and the threads and .join() can timeout and if the main thread crashes, then it will all crash and restart automatically (by systemd).
logger.info('initialized TWCANMessagesClass thread')
self.start() # auto start on initialization
def stop(self):
logger.debug('TWCANMessagesClass thread stop requested')
self._stop_thread.set()
def run(self):
while True:
try:
self.message = TWCAN.recv(timeout=0.5) # need to time out so can break out of the loop and cleanly shutdown
except:
logger.exception('error with TWCAN.recv')
sleep(5)
else:
#####################################################################
# don't need proximity for these things since they come from TWCAN
#####################################################################
if (self.message is not None):
if (m3.get_message_by_name('ID21DCP_evseStatus').frame_id == self.message.arbitration_id):
self.TESLA_SWCAN_ESTABLISHED=(getCANvalue(self.message.data,'ID21DCP_evseStatus','CP_teslaSwcanState')=="TESLA_SWCAN_ESTABLISHED")
if getCANvalue(self.message.data,'ID21DCP_evseStatus','CP_acChargeState')=="AC_CHARGE_ENABLED":
self.AC_CHARGE_ENABLED=True
elif getCANvalue(self.message.data,'ID21DCP_evseStatus','CP_acChargeState')=="AC_CHARGE_STANDBY":
self.AC_CHARGE_ENABLED=False
else:
#don't know, need to handle this better!
pass
elif (m3.get_message_by_name('ID292BMS_SOC').frame_id == self.message.arbitration_id):
# note, this is a few percent higher than what actually comes up in the car. there are other percent based SOC values, but they are even higher.
# ID33AUI_rangeSOC, UI_Range does match what is in the car though, but it is in units of miles. can this be simply converted to percent??????
self.StateOfCharge=getCANvalue(self.message.data,'ID292BMS_SOC','SOCUI292')
#can pickup on either TW or SW CAN, but better to pickup on TW can because the wall unit can't inject bogus data onto that bus
#also, if picking up on SW CAN, need to do it after the "if Proximity:" statement below.
elif (self.message.arbitration_id == 0x3d2): #0x syntax seems to automatically convert to an integer.
#Model3CAN.dbc seems to have this mixed up with kWhoursDischarged? can fix Model3CAN.dbc, but just keeping it this way as an excersise on how decoding actually works.
self.TotalWhoursCharged=int.from_bytes(self.message.data[0:4],byteorder='little') #seems to include regen?
self.TotalWhoursDischarged=int.from_bytes(self.message.data[4:8],byteorder='little') #not needed, but just keeping in here so understand what the rest of the message contains
elif (m3.get_message_by_name('ID31CCC_chgStatus').frame_id == self.message.arbitration_id):
self.Volts=getCANvalue(self.message.data,'ID31CCC_chgStatus','CC_line1Voltage')
self.MaxAmps=getCANvalue(self.message.data,'ID31CCC_chgStatus','CC_currentLimit')
elif (m3.get_message_by_name('ID32CCC_logData').frame_id == self.message.arbitration_id):
if getCANvalue(self.message.data,'ID32CCC_logData','CC_logIndex') == 'Mux1': #Signals available in the message seem to be dependent on this value.
self.Amps=getCANvalue(self.message.data,'ID32CCC_logData','CC_conn1Current')
#END can pickup on either TW or SW CAN, but better to pickup on TW can because the wall unit can't inject bogus data onto that bus
#####################################################################
if self._stop_thread.is_set():
break
logger.info('stopped TWCANMessagesClass thread')
class ThreadManagerClass:
def __init__(self):
self.ThreadList=[]
self.JoinTimeout=10
self.ShutdownRequested=False
self.CleanShutdown=True
def AddThread(self,TheThread):
self.ThreadList.append(TheThread)
def StopThreads(self):
logger.info('shutdown requested')
self.ShutdownRequested=True
logger.debug('shutting threads down')
# tell all threads to stop
for TheThread in self.ThreadList:
TheThread.stop()
# now, wait for them to stop
# note: if .join is not used with GUI, python tries too quit before the stop command is received by the thread and it gracefully shutdown and then it takes longer for tk to timeout and close the interpreter (it seems that is what is going on at least).
# note: .join(self.JoinTimeout) returns after self.JoinTimeout seconds OR when the thread joins/quits, whichever is sooner.
# so, need to check .is_alive() to see if the thread actually is still running.
for TheThread in self.ThreadList:
TheThread.join(self.JoinTimeout)
logger.debug('threads should be shut down')
def AnyThreadAlive(self):
for TheThread in self.ThreadList:
if TheThread.is_alive():
return True
else:
return False
################################################################
################################################################
# start up threads
################################################################
ReceiveInvoicesThread=ReceiveInvoices()
SWCANMessages=SWCANMessagesClass()
TWCANMessages=TWCANMessagesClass()
ThreadManager=ThreadManagerClass()
ThreadManager.AddThread(GUI)
ThreadManager.AddThread(SWCANMessages)
ThreadManager.AddThread(TWCANMessages)
ThreadManager.AddThread(ReceiveInvoicesThread)
################################################################
# hack: create an empty Meter class to use as a placeholder until the real Meter function is ported from GRID to EV so that LogData can work
class Meter: pass
while True:
try:
#pass values to the GUI
GUI.Volts=TWCANMessages.Volts
GUI.Amps=TWCANMessages.Amps
GUI.Power=Power
GUI.BigStatus=BigStatus
GUI.SmallStatus=SmallStatus
GUI.EnergyDelivered=EnergyDelivered
GUI.EnergyCost=EnergyDelivered*CurrentRate
GUI.CreditRemaining=(EnergyPaidFor-EnergyDelivered)*CurrentRate
GUI.RecentRate=CurrentRate
GUI.RequiredPaymentAmount=RequiredPaymentAmountAccepted
GUI.ChargeStartTime=ChargeStartTime
GUI.Connected=Proximity
GUI.MaxAmps=TWCANMessages.MaxAmps
GUI.SettledPayments=EnergyPaidFor*CurrentRate
if TWCANMessages.TESLA_SWCAN_ESTABLISHED:
SWCANActive=True
else: #disconnect on anything else for now (may want to revisit all states and see if want to stay connected on sleep for example)
#does not seem to send another signal before going into sleep mode. need to figure out something else to do to detect, or also use voltage measured
#from labjack on pilot/proximity pin to have more confidence on what is going on, like how the wall unit operates.
#causes problems and then car errors out even though SWCAN is actually active, canbus doesn't think so, so ....
#also need to consider having a 15 second delay between pluggin/unplugging like the wall unit, so that they are both measuring energy delivery from the same start time
SWCANActive=False
TWCANMessages.Volts=None
TWCANMessages.Amps=None
Power=0
TWCANMessages.MaxAmps=0
if SWCANActive and not Proximity:
Proximity=True
logger.info("plug inserted")
BigStatus='Charge Cable Inserted'
SmallStatus=''
CurrentTime=time()
TotalWhoursCharged_start=-1
EnergyDelivered=0
EnergyPaidFor=0
NumberOfPaymentsReceived=0
ChargeStartTime=datetime.now()
AcceptedRate=False
DataLogger=LogData(Meter,GUI)
elif Proximity: # already have Proximity, but something changed.
if TWCANMessages.AC_CHARGE_ENABLED and SWCANActive:
if not SWCAN_Relay.is_lit:
SWCAN_Relay.on()
logger.debug("relay energized")
if BigStatus=='Charging Idle':
logger.debug('Charging Resume From Idle')
BigStatus='Charging'
SmallStatus=''
else:
if SWCAN_Relay.is_lit:
SWCAN_Relay.off()
logger.debug("relay off")
# clear message values received from the bus. otherwise an old offer will be accepted when re-plugging in the bus before the relay is
# even energized and/or before wall sends an offer and then wall will never get an acceptance message.
#is there any kind of delay needed here in case a new message value is received while the relay is mechanically de-energizing?
SWCANMessages.WhoursPerPayment=None
SWCANMessages.RequiredPaymentAmount=None
if SWCANActive:
logger.debug('Charging Idle')
BigStatus='Charging Idle'
SmallStatus='Waiting For Car To Resume Charging'
if not SWCANActive:
Proximity=False
DataLogger.close()
logger.debug("plug removed\n\n\n")
BigStatus='Charge Cable Removed'
SmallStatus=''
sleep(2)
BigStatus='Insert Charge Cable Into Car'
SmallStatus='Waiting For Charge Cable To Be Inserted'
if Proximity:
#################################################################
# do this stuff before testing for AcceptedRate because want to
# still monitor power and energy if not paying via distributed charge.
#################################################################
if TWCANMessages.TotalWhoursCharged !=-1:
if TotalWhoursCharged_start==-1: # just plugged in
TotalWhoursCharged_start=TWCANMessages.TotalWhoursCharged
#not yet used. need to add to the GUI or some other kind of report. can help understand how much energy is wasted warming the battery up as well as charger
#efficinecy since the Tesla GUI is very misleading on how much energy you are actually using
EnergyAddedToBattery=TWCANMessages.TotalWhoursCharged-TotalWhoursCharged_start
if (TWCANMessages.Volts is not None) and (TWCANMessages.Amps is not None): #can't start doing anything until an initial voltage and current reading is obtained on the can bus because need that to decide when to pay.
PreviousTime=CurrentTime
CurrentTime=time()
deltaT=(CurrentTime-PreviousTime)/3600 #hours, small error on first loop when Proximity is initially True
Power=TWCANMessages.Volts*TWCANMessages.Amps
EnergyDelivered+=deltaT*Power #W*hours
#################################################
# hack: define values for the Meter class to use
# as a placeholder until the real Meter class
# is ported from GRID to EV so that LogData can work
#################################################
Meter.Power=Power
Meter.Volts=TWCANMessages.Volts
Meter.Amps=TWCANMessages.Amps
Meter.EnergyDelivered=EnergyDelivered
Meter.EnergyCost=EnergyDelivered*CurrentRate
Meter.RecentRate=CurrentRate
Meter.SalePeriods = 1
Meter.SellOfferTerms = {
'OfferStartTime' : time(),
'OfferStopTime' : time(),
}
Meter.BuyOfferTerms = {
'RateInterpolator' : None,
}
#################################################
#################################################################
if AcceptedRate:
if len(ReceiveInvoicesThread.InvoiceQueue)>0: #invoices are waiting to be paid
oldestInvoice=ReceiveInvoicesThread.InvoiceQueue.popleft()
logger.debug("decoding "+oldestInvoice)
AmountRequested=int(decode(oldestInvoice).amount/1000)
logger.info("seller wants to be paid "+str(AmountRequested)+" satoshis")
SmallStatus='Payment Requested'
AllowedError=(0.025-0.20)/(48-5)*(TWCANMessages.Amps-5)+0.2 #measurement error seems to be somewhat linear between car and charger. need to further investigate.
# note, this formula assumes the current is constant throughout the session. if the current is initially low and then goes up, it might not work because the new current is used for all former error.
if ( # TODO as noted elsewhere, need rework this to be in sat not W*hour
((EnergyPaidFor-EnergyDelivered)<(WhoursPerPaymentAccepted*0.70*2+(EnergyDelivered*AllowedError+75))) #not asking for payment before energy is delivered (allowed to pay after 30% has been delivered (70% ahead of time)---actually, poor internet connections can be very slow, so make this 140% ahead instead. also tolerate error, including a linear error and a fixed error that is a little generous right now but occurs during initial plug in because the car and wall unit start measuring at slightly different times.
and
(
(AmountRequested<=RequiredPaymentAmountAccepted) #not asking for too much payment
or
(
(AmountRequested<=2*RequiredPaymentAmountAccepted)
and
(EnergyPaidFor==0) #first payment allows 2x normal payment amount.
)
)
): #if all good, then it's time to send another invoice
try:
LNDBalance=lnd.channel_balance().local_balance.sat
logger.info('LND (off chain) account balance : '+RoundAndPadToString(LNDBalance,0)+' sat')
except:
logger.exception('tried getting LND (off chain) account balance but there was probably a network connection issue.')
ReceiveInvoicesThread.InvoiceQueue.appendleft(oldestInvoice) #put the invoice back in the queue
sleep(2)
else:
if LNDBalance<AmountRequested*(1+MaxFeeFraction):
logger.error('LND (off chain) account balance is too low')
sleep(20)
else:
try:
MaxAllowableFee=max(ceil(AmountRequested*MaxFeeFraction),MaxFeeSat)
logger.info("sending payment for "+RoundAndPadToString(AmountRequested,0)+" sat with a max allowable fee of "+RoundAndPadToString(MaxAllowableFee,0)+' sat ('+RoundAndPadToString(100*(MaxAllowableFee/AmountRequested),2)+'%)')
# should check to make sure the "expiry" has not passed on the invoice yet before paying????
# allow changing final_cltv_delta of the sent payment?
# can to be a very long time until timeout on network failure so this exception isn't caught very quickly and the GUI never updates while it is waiting. might want to move this to another thread???
# send the payment and display updates as they are streamed during routing and settlement
for PaymentResponse in lnd.send_payment_v2(payment_request=oldestInvoice, fee_limit_sat=MaxAllowableFee, timeout_seconds=25, allow_self_payment=True):
logger.debug('====================================================================================\n' + indent(DocumentWrapper(subsequent_indent=' ',width=80).fill(str(PaymentResponse)),' '*53))
if PaymentResponse.status != 2:
raise Exception('payment did not succeed, status is '+str(PaymentResponse.status))
except:
logger.exception("tried sending payment but there was an issue")
ReceiveInvoicesThread.InvoiceQueue.appendleft(oldestInvoice) #put the invoice back in the queue
sleep(2)
else:
logger.info('sent payment: total fees = '+RoundAndPadToString(PaymentResponse.fee_sat,0)+' [sat] ('+RoundAndPadToString(100*(PaymentResponse.fee_sat/AmountRequested),2)+'%)')
logger.info('total outstanding invoices is now '+str(len(ReceiveInvoicesThread.InvoiceQueue)))
# TODO as noted elsewhere, need rework this to be in sat not W*hour
EnergyPaidFor+=AmountRequested/CurrentRate
NumberOfPaymentsReceived+=1
#################################################
# hack: define values for the Meter class to use
# as a placeholder until the real Meter class
# is ported from GRID to EV so that LogData can work
#################################################
Meter.EnergyPayments = EnergyPaidFor*CurrentRate
Meter.NumberOfPaymentsReceived = NumberOfPaymentsReceived
#################################################
DataLogger.LogTabularDataAndMessages()
logger.info('Car Battery State Of Charge: '+ RoundAndPadToString(TWCANMessages.StateOfCharge,2)+'%')
SmallStatus='Payment Sent'
else:
#seller is asking for payment to quickly, waiting until they deliver energy that was agreed upon.
#if they aren't happy and think they delivered enough, they will shut down.
#currently, the buyer and seller will both tolerate some error.
#because they need to give time for a payment to actually be made and account for their different instrumentation.
#need to do something if AmountRequested>RequiredPaymentAmountAccepted and EnergyPaidFor>0 ????????????????? don't remember what this comment was about.....
logger.debug("not yet time to pay, waiting")
ReceiveInvoicesThread.InvoiceQueue.appendleft(oldestInvoice) #put the invoice back in the queue
sleep(2)
else:
# logger.debug("waiting for next invoice")
pass
elif (SWCANMessages.WhoursPerPayment is not None) and (SWCANMessages.RequiredPaymentAmount is not None): #offer received
# make a copy so don't let a new value on the bus change what is enforced locally
WhoursPerPaymentAccepted=SWCANMessages.WhoursPerPayment
RequiredPaymentAmountAccepted=SWCANMessages.RequiredPaymentAmount
CurrentRate=RequiredPaymentAmountAccepted/WhoursPerPaymentAccepted #1/(Whours_offered/for_sat)
if (CurrentRate<MaxRate) and (RequiredPaymentAmountAccepted<MaxRequiredPaymentAmount): #accept the rate, until SWCAN goes down. probably need to upgrade to allow rate changes during a charging session, but for now, this is how it works.
ReceiveInvoicesThread.InvoiceQueue.clear() #all previous invoices are no longer be valid as far as the buyer is concerned, so ignore them
AcceptedRate=True
#print('getting ready to accept an offer')
SWCAN.send(can_Message(arbitration_id=1999,data=[True],is_extended_id=False))
logger.info("accepted an offer of "+RoundAndPadToString(WhoursPerPaymentAccepted,1)+" W*hour for a payment of "+str(RequiredPaymentAmountAccepted)+" satoshis ["+RoundAndPadToString(CurrentRate,1)+" satoshis/(W*hour)]")
BigStatus='Charging'
SmallStatus='Accepted Sale Terms'
else: #don't accept the rate, it's too high. wait and see if a lower offer is made.
SWCAN.send(can_Message(arbitration_id=1999,data=[False],is_extended_id=False))
logger.info("rate or payment amount too high, not accepting")
SmallStatus='Rejected Sale Terms, Waiting for a Better Offer'
# don't check again until a new offer actually comes in
SWCANMessages.WhoursPerPayment=None
SWCANMessages.RequiredPaymentAmount=None
#provide more detail in outputs on why was not accepted
else:
#continue to wait for an offer
pass
# shutdown logic
if not ThreadManager.ShutdownRequested and GUI.stopped() and not GUI.is_alive():
logger.info('GUI triggered shutdown request')
# need to re-call SystemExit outside of the GUI thread
sys.exit()
if ThreadManager.AnyThreadAlive():
if ThreadManager.ShutdownRequested:
logger.error('all threads did not shut down on their own, terminating the remaining threads')
break
else:
# nothing to do, not time to shut down
sleep(.1)
else:
if ThreadManager.ShutdownRequested:
logger.debug('all threads shut down on their own after being asked')
break
else:
logger.debug('all threads shut down on their own without being asked')
break
#also add a check for one thread shut down on it's own and then force shutdown of everything since things probably won't work right with a dead thread?????? that could then generalize things a bit because wouldn't need to specifically check for the GUI thread to have stopped?
except (KeyboardInterrupt, SystemExit):
ThreadManager.StopThreads()
# now loop again to see above if the threads shutdown after being asked
# does this cause finally to be executed twice then???? yes, seems it does. also seems like a more confusing way to write the logic above instead of below, so may want to re-do this.
except:
logger.exception('error in main loop')
ThreadManager.CleanShutdown=False
#should this also run ThreadManager.StopThreads() to be cleaner?????
raise
finally:
if ThreadManager.ShutdownRequested or not ThreadManager.CleanShutdown: # don't run on every loop iteration, only if shutting down
# the state should be restored to off when python is stopped, but explicitly set to off to be sure.
SWCAN_Relay.off()
if not ThreadManager.CleanShutdown: # if an uncaught exception, put some extra lines at the end
ExtraText='\n\n\n'
else:
ExtraText=''
logger.info("turned off SWCAN relay"+ExtraText)
logger.info('shutdown complete\n\n\n')