-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathwall
executable file
·493 lines (309 loc) · 17.8 KB
/
wall
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
#!/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='wall'
# dc.common must be the second imported module because it reads the config
from dc.common import ConfigFile, ProximityAnalogVoltage, can_Message, SWCAN_Relay, SWCAN, SWCAN_ISOTP, GUI, logger, AddAndWatchInvoice, SMTPNotification
from time import sleep,time
from datetime import datetime,timedelta
from helpers2 import FormatTimeDeltaToPaddedString,RoundAndPadToString
import sys
from pathlib import Path
sys.path.append(str(Path.home())+"/Desktop/TWCManager/")
import TWCManager
################################################################
#define configuration related constants
################################################################
ProximityVoltage=1.5 #Voltage that indicates charge cable has been plugged in
ProximityVoltageTolerance=0.05*2*2.5 #need the extra *2.5 for 208V power? was getting 1.289V every once and a while and not sure why! so, it was disconnecting and screwing everything up.
CurrentRate=1 #sat/(W*hour)
WhoursPerPayment=int(25) #W*hour/payment
RequiredPaymentAmount=int(WhoursPerPayment*CurrentRate) #sat/payment
################################################################
################################################################
# initialize variables
################################################################
ShutdownRequested=False
CleanShutdown=True
Proximity=False
OfferAccepted=False
ReInsertedMessagePrinted=False
ProximityCheckStartTime=-1
ProximityLostTime=0
Volts=None
Amps= None
Power=0
TimeLastOfferSent=time()
ChargeStartTime=-1
ChargeTimeText=FormatTimeDeltaToPaddedString(timedelta(seconds=0))
EnergyDelivered=0
EnergyPaidFor=0
EnergyCredit=0
BigStatus='Insert Charge Cable Into Car'
SmallStatus='Waiting For Charge Cable To Be Inserted'
################################################################
TWCManager.MainThread.start()
while len(TWCManager.master.slaveTWCRoundRobin)<1: #wait until connected to a wall unit.
#this is not deterministic though because seems to disconnect sometimes, especially when using crontab to lauch the scrpt on boot (which is really weird).
sleep(.1)
myTWCID=TWCManager.master.slaveTWCRoundRobin[0].TWCID #only one wall unit now, so using number 0 blindly
logger.info('TWCID: '+str(myTWCID))
logger.info("should be connected to the TWC")
####################
#not positive these are needed here since will be done below with every instance of the loop, but do it anyway because want something defined before sendStartCommand
TWCManager.master.setChargeNowAmps(ConfigFile['Seller']['MaxAmps']) #set maximum current. need to refine this statement if have multiple wall units.
TWCManager.master.setChargeNowTimeEnd(int(3600)) #set how long to hold the current for, in seconds. need to refine this statement if have multiple wall units.
####################
TWCManager.master.sendStartCommand() #need to refine this statement if have multiple wall units.
TWCManager.master.settings['chargeStopMode']=3
TWCManager.master.saveSettings()
################################################################
while True:
try:
#pass values to the GUI
GUI.Volts=Volts
GUI.Amps=Amps
GUI.Power=Power
GUI.BigStatus=BigStatus
GUI.SmallStatus=SmallStatus
GUI.EnergyDelivered=EnergyDelivered
GUI.SettledPayments=EnergyPaidFor*CurrentRate
GUI.EnergyCost=EnergyDelivered*CurrentRate
GUI.CreditRemaining=EnergyCredit*CurrentRate
GUI.RecentRate=CurrentRate
GUI.RequiredPaymentAmount=RequiredPaymentAmount
GUI.ChargeStartTime=ChargeStartTime
GUI.Connected=Proximity
GUI.MaxAmps=ConfigFile['Seller']['MaxAmps']
#not sure how to make ChargeNow perpetual, so just add an hour on every loop.
TWCManager.master.setChargeNowTimeEnd(int(3600)) #set how long to hold the current for, in seconds. need to refine this statement if have multiple wall units.
TheOutputVoltage=ProximityAnalogVoltage()
if (TheOutputVoltage > ProximityVoltage-ProximityVoltageTolerance) and (not Proximity):
if (time()>ProximityLostTime+15): #wait at least 15 seconds after the plug was removed to start looking for proximity again
if ProximityCheckStartTime==-1:
ProximityCheckStartTime=time()
logger.info("plug inserted") #or was already inserted, but finished waiting 15 seconds
BigStatus='Charge Cable Inserted'
SmallStatus=''
elif time()>ProximityCheckStartTime+3: #proximity must be maintained for at least 3 seconds
Proximity=True
ReInsertedMessagePrinted=False
ProximityCheckStartTime=-1
SWCAN_Relay.on()
logger.info("relay energized")
CurrentTime=time()
InitialInvoice=True
EnergyDelivered=0
EnergyPaidFor=0
EnergyCredit=0
elif not ReInsertedMessagePrinted:
logger.info("plug re-inserted in less than 15 seconds, waiting")
#need to add this waiting logic to the car unit as well, so that both wall and car unit are in sync and start measuring energy delivery at the same time.
BigStatus='Waiting'
SmallStatus=''
ReInsertedMessagePrinted=True
logger.info('Proximity Voltage: '+str(TheOutputVoltage))
elif (TheOutputVoltage < ProximityVoltage-ProximityVoltageTolerance*2) and (not Proximity) and (ProximityCheckStartTime!=-1 or ReInsertedMessagePrinted):
ProximityLostTime=time()
ReInsertedMessagePrinted=False
ProximityCheckStartTime=-1
logger.info("plug was removed before the relay was energized")
logger.info('Proximity Voltage: '+str(TheOutputVoltage))
BigStatus='Charge Cable Removed'
SmallStatus=''
sleep(2)
BigStatus='Insert Charge Cable Into Car'
SmallStatus='Waiting For Charge Cable To Be Inserted'
elif (TheOutputVoltage < ProximityVoltage-ProximityVoltageTolerance*2) and (Proximity):
Proximity=False
ProximityLostTime=time()
logger.info("plug removed\n\n\n")
logger.info('Proximity Voltage: '+str(TheOutputVoltage))
BigStatus='Charge Cable Removed'
SmallStatus=''
sleep(2)
BigStatus='Insert Charge Cable Into Car'
SmallStatus='Waiting For Charge Cable To Be Inserted'
#reset the values of current and voltage....actually, may not be needed here anymore ? need to remove and test.
Volts = None
Amps = None
Power = 0
SWCAN_Relay.off()
try:
#have to reference each time because the slave is destroyed and created if disconnected, and sometimes it disconnects
Volts=TWCManager.master.slaveTWCs[myTWCID].voltsPhaseA
Amps=TWCManager.master.slaveTWCs[myTWCID].reportedAmpsActual
except:
Volts=-99
Amps=-99
if Proximity:
message = SWCAN.recv(timeout=.075)
if PowerKilled:
BigStatus='Stopped Charging'
ChargeStartTime=-1 #makes stop counting charge time even through there is still proximity
else:
if (Volts is not None) and (Amps is not None):
#need to do more rigorous testing of a stable connection to the TWC above instead before getting to this point. especially since None can be reset to somethign else like -99.
PreviousTime=CurrentTime
CurrentTime=time()
deltaT=(CurrentTime-PreviousTime)/3600 #hours, small error on first loop when SWCANActive is initially True
Power=Volts*Amps
EnergyDelivered+=deltaT*Power #W*hours
EnergyCredit = EnergyPaidFor-EnergyDelivered # W*hours
if OfferAccepted:
if PendingInvoice:
#check to see if the current invoice has been paid
try:
if OutstandingInvoice.state==1:
# TODO as noted below, need rework this to be in sat not W*hour
EnergyPaidFor+=OutstandingInvoice.value/(CurrentRate) #W*hours
# also re-calculate here if a payment was just received since changing the value of InitialInvoice needs to know the real EnergyCredit before repeating the loop.
EnergyCredit = EnergyPaidFor-EnergyDelivered # W*hours
PendingInvoice=False
InitialInvoice=False #reset every time just to make the logic simpler
logger.info("payment received, time since last payment received="+str(time()-LastPaymentReceivedTime)+"s")
ChargeTimeText=FormatTimeDeltaToPaddedString(timedelta(seconds=round((datetime.now()-ChargeStartTime).total_seconds()))) #round to the nearest second, then format as a zero padded string
logger.info('WhoursCreditRemaining: '+RoundAndPadToString(EnergyCredit,1)+', WhoursDelivered: '+RoundAndPadToString(EnergyDelivered,1)+', Volts: '+RoundAndPadToString(Volts,2)+', Amps: '+RoundAndPadToString(Amps,2)+', ChargeSessionTime: '+ChargeTimeText)
LastPaymentReceivedTime=time()
SmallStatus='Payment Received'
except:
logger.info("tried checking the current invoice's payment status but there was probably a network connection issue")
sleep(.25)
#now that the pending invoices have been processed, see if it's time to send another invoice, or shutdown power if invoices haven't been paid in a timely manner.
#time to send another invoice
#adjust multiplier to decide when to send next invoice. can really send as early as possible because car just waits until it's really time to make a payment.
#was 0.5, but higher is probably really better because don't know how long the lightning network payment routing is actually going to take.
#send payment request 2*90% ahead of time so the buyer can have it ready in case they have a poor internet connection and want to pay early to avoid disruptions.
#note, because below 1% error is allowed, this test may actually not have much meaning considering over the course of a charging cycle
#the total error may be larger than an individual payment amount, so EnergyCredit is likely less than 0 and therefor
#a new invoice will just be sent right after the previous invoice was paid, rather than waiting.
# TODO: rework this. it is in terms of W*hour on the left and sat on the right. this is wrong but the rate is currently 1 sat/(W*hour), so it works out.
# in the future with variable rates like GRID, will not be able to have a known energy amount that has been prepaid for, instead need to just have a fixed
# prepayment amount in sat.
if ((EnergyCredit)<RequiredPaymentAmount*2*0.90) and not PendingInvoice:
RequiredPaymentAmount=WhoursPerPayment*CurrentRate #sat
try:
OutstandingInvoice=AddAndWatchInvoice(RequiredPaymentAmount,'Distributed Charge Energy Payment')
#need to add a try except here, because the buyer may not be listening properly!!!!!!!!!!!!
SWCAN_ISOTP.send(OutstandingInvoice.payment_request.encode()) #send the new invoice using CAN ISOTP
logger.info("sent new invoice for "+str(RequiredPaymentAmount)+" satoshis")
SmallStatus='Payment Requested'
PendingInvoice=True
except:
logger.info("tried getting a new invoice but there was probably a network connection issue")
sleep(.25)
elif PendingInvoice: #waiting for payment
#logger.info("waiting for payment, and limit not yet reached")
pass
else:
#logger.info("waiting to send next invoice")
pass
else:
#try to negotiate the offer
if (message is not None) and (message.arbitration_id == 1999 and message.data[0]==1): #don't really need to convert to int since hex works fine for a 0 vs 1
try:
FirstRequiredPaymentAmount=1*RequiredPaymentAmount #sat, adjust multiplier if desire the first payment to be higher than regular payments.
OutstandingInvoice=AddAndWatchInvoice(FirstRequiredPaymentAmount,'Distributed Charge Energy Payment')
#buyer accepted the rate
OfferAccepted=True
logger.info("buyer accepted rate")
ChargeStartTime=datetime.now()
BigStatus='Charging'
SmallStatus='Sale Terms Accepted'
SWCAN_ISOTP.send(OutstandingInvoice.payment_request.encode())
logger.info("sent first invoice for "+str(FirstRequiredPaymentAmount)+" satoshis")
PendingInvoice=True
except:
#probably the protocol will get stuck if this exception is caught because the buyer won't re-send the acceptance message. maybe need buyer to keep repeating the acceptance message until the seller sends an acknowledgement???
# logger.info("tried getting a new (first) invoice but there was probably a network connection issue")
# sleep(.25)
raise
elif (message is not None) and (message.arbitration_id == 1999 and message.data[0]==0):
OfferAccepted=False
logger.info("buyer rejected rate")
#SmallStatus='Sale Terms Rejected'
elif (TimeLastOfferSent+1)<time(): #only send once per second and give the outer loop a chance to process all incoming messages, otherwise will send multiple offers in between the tesla messages and the car module messages.
#provide the offer
#need to do a try except here, because the buyer may not be listening properly
SWCAN.send(can_Message(arbitration_id=1998,data=int(WhoursPerPayment).to_bytes(4, byteorder='little')+int(RequiredPaymentAmount).to_bytes(4, byteorder='little'),is_extended_id=False))
TimeLastOfferSent=time()
logger.info("provided an offer of "+RoundAndPadToString(WhoursPerPayment,1)+" W*hour for a payment of "+str(RequiredPaymentAmount)+" satoshis ["+RoundAndPadToString(CurrentRate,1)+" satoshis/(W*hour)]")
SmallStatus='Provided An Offer'
#SmallStatus='Sale Terms Offered To Vehicle'
LastPaymentReceivedTime=time() #fudged since no payment actually received yet, but want to still time since invoice sent, and need variable to be initialized.
if (
#buyer must pay ahead 20% for all payments but the first payment (must pay after 80% has been delivered).
#also allow 1% error due to measurement error as well as transmission losses between the car and the wall unit.
#this error basically needs to be taken into consideration when setting the sale rate.
(((EnergyCredit)<WhoursPerPayment*0.20*1.01) and not InitialInvoice)
or
#buyer can go into debt 30% before the first payment, also allowing for 1% error as above, although that may be really needed for the first payment.
(((EnergyCredit)<-WhoursPerPayment*0.30*1.01) and InitialInvoice)
):
logger.info("buyer never paid, need to kill power, time since last payment received="+str(time()-LastPaymentReceivedTime)+"s, InitialInvoice="+str(InitialInvoice))
logger.info('WhoursCreditRemaining: '+RoundAndPadToString(EnergyCredit,1)+', WhoursDelivered: '+RoundAndPadToString(EnergyDelivered,1)+', Volts: '+RoundAndPadToString(Volts,2)+', Amps: '+RoundAndPadToString(Amps,2)+', ChargeSessionTime: '+ChargeTimeText)
PowerKilled=True #need to be smarter and make sure power is actually killed (monitor amps is one way) because it doesn't always seem to work. maybe it doesn't always work because need to also add myTWCID to the following command (and actually, should consider adding it above in some other commands as well?)?
TWCManager.master.sendStopCommand() #need to refine this statement if have multiple wall units.
SmallStatus='Vehicle Did Not Make Payment'
SMTPNotification('buyer never paid','')
else:
OfferAccepted=False
PowerKilled=False
sleep(.075*3) #make this longer than the receive timeouts so that the buffers always get empty, otherwise there will be a reaction delay because the receiver is still processing old messages????
# shutdown logic
# raise Exception('test exception')
if not ShutdownRequested and GUI.stopped() and not GUI.is_alive():
logger.info('GUI triggered shutdown request')
# need to re-call SystemExit outside of the thread
sys.exit()
# wait around until it is time to tell GUI to stop
if GUI.is_alive() or TWCManager.MainThread.is_alive():
if ShutdownRequested:
logger.error('threads did not shut down on their own, terminating them')
break
else:
# nothing to do
sleep(.1)
else:
if ShutdownRequested:
logger.debug('threads shut down on their own after being asked')
break
else:
logger.debug('threads shut down on their own without being asked')
break
except (KeyboardInterrupt, SystemExit):
logger.info('shutdown requested')
ShutdownRequested=True
logger.debug('shutting threads down')
TWCManager.MainThread.stop()
GUI.stop()
# note: .join(10) returns after 10 seconds OR when the thread joins/quits, whichever is sooner.
# so, need to check .is_alive() above to see if the thread actually is still running.
TWCManager.MainThread.join(10)
GUI.join(10) #for some reason if this is not used, 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?
# now go up to (not GUI.is_alive()) and ShutdownRequested
except:
logger.exception('error in main loop')
CleanShutdown=False
emailer=SMTPNotification('error in main loop','')
emailer.join() # allow the e-mail to actually send
raise
finally:
if ShutdownRequested or not CleanShutdown:
# the state should be restored to off when python is stopped, but explicitly set to off to be sure.
SWCAN_Relay.off()
if not CleanShutdown: # if an uncaught exception, put some extra lines at the end
ExtraText='\n\n\n'
else:
ExtraText=''
logger.info("turned off relay"+ExtraText)
logger.info('shutdown complete\n\n\n')