foxesscloud 2.5.1__py3-none-any.whl → 2.5.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foxesscloud/foxesscloud.py +167 -161
- foxesscloud/openapi.py +163 -153
- {foxesscloud-2.5.1.dist-info → foxesscloud-2.5.3.dist-info}/METADATA +20 -4
- foxesscloud-2.5.3.dist-info/RECORD +7 -0
- foxesscloud-2.5.1.dist-info/RECORD +0 -7
- {foxesscloud-2.5.1.dist-info → foxesscloud-2.5.3.dist-info}/LICENCE +0 -0
- {foxesscloud-2.5.1.dist-info → foxesscloud-2.5.3.dist-info}/WHEEL +0 -0
- {foxesscloud-2.5.1.dist-info → foxesscloud-2.5.3.dist-info}/top_level.txt +0 -0
foxesscloud/foxesscloud.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud
|
4
|
-
Updated:
|
4
|
+
Updated: 23 September 2024
|
5
5
|
By: Tony Matthews
|
6
6
|
"""
|
7
7
|
##################################################################################################
|
@@ -10,7 +10,7 @@ By: Tony Matthews
|
|
10
10
|
# ALL RIGHTS ARE RESERVED © Tony Matthews 2023
|
11
11
|
##################################################################################################
|
12
12
|
|
13
|
-
version = "1.6.
|
13
|
+
version = "1.6.5"
|
14
14
|
print(f"FoxESS-Cloud version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -19,6 +19,25 @@ debug_setting = 1
|
|
19
19
|
month_names = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
20
20
|
day_names = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
|
21
21
|
|
22
|
+
import os.path
|
23
|
+
import json
|
24
|
+
import time
|
25
|
+
from datetime import datetime, timedelta, timezone
|
26
|
+
from copy import deepcopy
|
27
|
+
import requests
|
28
|
+
from requests.auth import HTTPBasicAuth
|
29
|
+
import hashlib
|
30
|
+
import math
|
31
|
+
import matplotlib.pyplot as plt
|
32
|
+
|
33
|
+
fox_domain = "https://www.foxesscloud.com"
|
34
|
+
fox_client_id = "5245784"
|
35
|
+
fox_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
36
|
+
time_zone = 'Europe/London'
|
37
|
+
|
38
|
+
# optional path to use for file storage
|
39
|
+
storage = ''
|
40
|
+
|
22
41
|
# global plot parameters
|
23
42
|
figure_width = 9 # width of plots
|
24
43
|
legend_location = "upper right"
|
@@ -39,27 +58,6 @@ def plot_show():
|
|
39
58
|
plt.show()
|
40
59
|
return
|
41
60
|
|
42
|
-
import os.path
|
43
|
-
import json
|
44
|
-
import time
|
45
|
-
from datetime import datetime, timedelta, timezone
|
46
|
-
from copy import deepcopy
|
47
|
-
import requests
|
48
|
-
from requests.auth import HTTPBasicAuth
|
49
|
-
import hashlib
|
50
|
-
#from random_user_agent.user_agent import UserAgent
|
51
|
-
#from random_user_agent.params import SoftwareName, OperatingSystem
|
52
|
-
import math
|
53
|
-
import matplotlib.pyplot as plt
|
54
|
-
|
55
|
-
#software_names = [SoftwareName.CHROME.value]
|
56
|
-
#operating_systems = [OperatingSystem.WINDOWS.value, OperatingSystem.LINUX.value]
|
57
|
-
#user_agent_rotator = UserAgent(software_names=software_names, operating_systems=operating_systems, limit=100)
|
58
|
-
|
59
|
-
fox_domain = "https://www.foxesscloud.com"
|
60
|
-
fox_client_id = "5245784"
|
61
|
-
fox_user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
62
|
-
time_zone = 'Europe/London'
|
63
61
|
|
64
62
|
##################################################################################################
|
65
63
|
##################################################################################################
|
@@ -221,11 +219,11 @@ token_renewal = timedelta(hours=2).seconds # interval before token needs t
|
|
221
219
|
|
222
220
|
# login and get token if required. Check if token has expired and renew if required.
|
223
221
|
def get_token():
|
224
|
-
global username, password, fox_user_agent, fox_client_id, time_zone, token_store, device_list, device, device_id, debug_setting, token_save, token_renewal, messages
|
222
|
+
global username, password, fox_user_agent, fox_client_id, time_zone, token_store, device_list, device, device_id, debug_setting, token_save, token_renewal, messages, storage
|
225
223
|
if token_store is None:
|
226
224
|
token_store = {'token': None, 'valid_from': None, 'valid_for': token_renewal, 'user_agent': fox_user_agent, 'lang': 'en', 'time_zone': time_zone, 'client_id': fox_client_id}
|
227
|
-
if token_store['token'] is None and os.path.exists(token_save):
|
228
|
-
file = open(token_save)
|
225
|
+
if token_store['token'] is None and os.path.exists(storage + token_save):
|
226
|
+
file = open(storage + token_save)
|
229
227
|
token_store = json.load(file)
|
230
228
|
file.close()
|
231
229
|
if token_store.get('time_zone') is None:
|
@@ -260,7 +258,7 @@ def get_token():
|
|
260
258
|
output(f"** no token in result data")
|
261
259
|
token_store['valid_from'] = time_now.isoformat()
|
262
260
|
if token_save is not None :
|
263
|
-
file = open(token_save, 'w')
|
261
|
+
file = open(storage + token_save, 'w')
|
264
262
|
json.dump(token_store, file, indent=4, ensure_ascii=False)
|
265
263
|
file.close()
|
266
264
|
return token_store['token']
|
@@ -834,13 +832,15 @@ merge_settings = { # keys to add
|
|
834
832
|
'WorkMode': {'keys': {
|
835
833
|
'h115__': 'operation_mode__work_mode',
|
836
834
|
'h116__': 'operation_mode__work_mode',
|
837
|
-
'h117__': 'operation_mode__work_mode'
|
835
|
+
'h117__': 'operation_mode__work_mode',
|
836
|
+
# 'k106__': 'operation_mode__work_mode',
|
838
837
|
},
|
839
838
|
'values': ['SelfUse', 'Feedin', 'Backup']},
|
840
839
|
'BatteryVolt': {'keys': {
|
841
840
|
'h115__': ['h115__14', 'h115__15', 'h115__16'],
|
842
841
|
'h116__': ['h116__15', 'h116__16', 'h116__17'],
|
843
|
-
'h117__': ['h117__15', 'h117__16', 'h117__17']
|
842
|
+
'h117__': ['h117__15', 'h117__16', 'h117__17'],
|
843
|
+
# 'k106__': ['k106__xx', 'k106__xx', 'k106__xx'],
|
844
844
|
},
|
845
845
|
'type': 'list',
|
846
846
|
'valueType': 'float',
|
@@ -849,6 +849,7 @@ merge_settings = { # keys to add
|
|
849
849
|
'h115__': 'h115__17',
|
850
850
|
'h116__': 'h116__18',
|
851
851
|
'h117__': 'h117__18',
|
852
|
+
# 'k106__': 'k106__xx',
|
852
853
|
},
|
853
854
|
'type': 'list',
|
854
855
|
'valueType': 'int',
|
@@ -1372,7 +1373,7 @@ sample_time = 5.0 # 5 minutes default
|
|
1372
1373
|
sample_rounding = 2 # round to 30 seconds
|
1373
1374
|
|
1374
1375
|
def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, plot=0, station=0):
|
1375
|
-
global token, device_id, debug_setting, var_list, invert_ct2, tariff, max_power_kw, messages, sample_rounding, sample_time
|
1376
|
+
global token, device_id, debug_setting, var_list, invert_ct2, tariff, max_power_kw, messages, sample_rounding, sample_time, storage
|
1376
1377
|
if station == 0 and get_device() is None:
|
1377
1378
|
return None
|
1378
1379
|
elif station == 1 and get_site() is None:
|
@@ -1420,12 +1421,12 @@ def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, p
|
|
1420
1421
|
output(f"** get_raw(), no raw data, {errno_message(errno)}")
|
1421
1422
|
return None
|
1422
1423
|
else:
|
1423
|
-
file = open(load)
|
1424
|
+
file = open(storage + load)
|
1424
1425
|
result = json.load(file)
|
1425
1426
|
file.close()
|
1426
1427
|
if save is not None:
|
1427
1428
|
file_name = save + "_raw_" + time_span + "_" + d[0:10].replace('-','') + ".txt"
|
1428
|
-
file = open(file_name, 'w', encoding='utf-8')
|
1429
|
+
file = open(storage + file_name, 'w', encoding='utf-8')
|
1429
1430
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1430
1431
|
file.close()
|
1431
1432
|
for var in result:
|
@@ -1497,7 +1498,7 @@ def get_raw(time_span='hour', d=None, v=None, summary=1, save=None, load=None, p
|
|
1497
1498
|
if e > 0.0:
|
1498
1499
|
kwh += e
|
1499
1500
|
if tariff is not None:
|
1500
|
-
if hour_in (h, [tariff.get('off_peak1'), tariff.get('off_peak2'), tariff.get('off_peak3')]):
|
1501
|
+
if hour_in (h, [tariff.get('off_peak1'), tariff.get('off_peak2'), tariff.get('off_peak3'), tariff.get('off_peak4')]):
|
1501
1502
|
kwh_off += e
|
1502
1503
|
elif hour_in(h, [tariff.get('peak1'), tariff.get('peak2')]):
|
1503
1504
|
kwh_peak += e
|
@@ -1755,12 +1756,12 @@ def get_report(report_type='day', d=None, v=None, summary=1, save=None, load=Non
|
|
1755
1756
|
for x in v:
|
1756
1757
|
result.append({'variable': x, 'data': [], 'date': d})
|
1757
1758
|
if load is not None:
|
1758
|
-
file = open(load)
|
1759
|
+
file = open(storage + load)
|
1759
1760
|
result = json.load(file)
|
1760
1761
|
file.close()
|
1761
1762
|
elif save is not None:
|
1762
1763
|
file_name = save + "_rep_" + report_type + "_" + d.replace('-','') + ".txt"
|
1763
|
-
file = open(file_name, 'w', encoding='utf-8')
|
1764
|
+
file = open(storage + file_name, 'w', encoding='utf-8')
|
1764
1765
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1765
1766
|
file.close()
|
1766
1767
|
if summary == 0:
|
@@ -2348,56 +2349,36 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2348
2349
|
strategy.append(prices[t])
|
2349
2350
|
output(f" {format_period(prices[t])} at {prices[t]['price']:.1f}p", 1)
|
2350
2351
|
tariff['agile']['strategy'] = strategy
|
2351
|
-
for key in ['off_peak1', 'off_peak2', 'off_peak3']:
|
2352
|
+
for key in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2352
2353
|
if tariff.get(key) is None:
|
2353
2354
|
continue
|
2354
2355
|
if tariff['agile'].get(key) is None:
|
2355
2356
|
tariff['agile'][key] = {}
|
2356
2357
|
# get price index for AM/PM charge times
|
2357
|
-
slots = []
|
2358
|
-
for i in range(0, len(prices)):
|
2359
|
-
if hour_in(time_hours(prices[i]['start']), tariff[key]):
|
2360
|
-
slots.append(i)
|
2358
|
+
slots = [i for i in range(0, len(prices)) if hour_in(time_hours(prices[i]['start']), tariff[key])]
|
2361
2359
|
tariff['agile'][key]['slots'] = slots
|
2362
|
-
|
2363
|
-
weighting = tariff_config.get('weighting')
|
2364
|
-
tariff['agile'][key]['times'] = []
|
2365
|
-
for j in range (0, len(slots)):
|
2366
|
-
span = j + 1
|
2367
|
-
weights = (([1.0] * (span-1) if weighting is None else weighting) + [0.5] * span)[:span]
|
2368
|
-
best = None
|
2369
|
-
price = None
|
2370
|
-
for i in range(0, len(slots) - j):
|
2371
|
-
t = slots[i: i + span]
|
2372
|
-
p_span = [prices[x]['price'] for x in t]
|
2373
|
-
wavg = round(sum(p * w for p,w in zip(p_span, weights)) / sum(weights), 2)
|
2374
|
-
if price is None or wavg < price:
|
2375
|
-
price = wavg
|
2376
|
-
best = t
|
2377
|
-
# save best time slot for charge duration
|
2378
|
-
start = prices[best[0]]['start']
|
2379
|
-
tariff['agile'][key]['times'].append({'start': start, 'end': round_time(start + span / 2), 'price': price, 'best': best, 'key': key})
|
2360
|
+
tariff['agile'][key]['avg'] = avg([prices[t]['price'] for t in slots])
|
2380
2361
|
# show the results
|
2381
2362
|
if tariff_config['show_data'] > 0:
|
2382
2363
|
data_wrap = tariff_config['data_wrap'] if tariff_config.get('data_wrap') is not None else 6
|
2383
2364
|
t = (now.hour * 2) % data_wrap
|
2384
|
-
s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t *
|
2365
|
+
s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t * 13
|
2385
2366
|
for i in range(0, len(prices)):
|
2386
2367
|
s += "\n" if i > 0 and t % data_wrap == 0 else ""
|
2387
|
-
s += f" {prices[i]['time']} {prices[i]['price']:4.1f}"
|
2368
|
+
s += f" {prices[i]['time']} {prices[i]['price']:4.1f},"
|
2388
2369
|
t += 1
|
2389
|
-
output(s)
|
2370
|
+
output(s[:-1])
|
2390
2371
|
if tariff_config['show_plot'] > 0:
|
2391
2372
|
plt.figure(figsize=(figure_width, figure_width/2))
|
2392
2373
|
x_timed = [i for i in range(0, len(prices))]
|
2393
2374
|
plt.xticks(ticks=x_timed, labels=[prices[x]['time'] for x in x_timed], rotation=90, fontsize=8, ha='center')
|
2394
2375
|
plt.plot(x_timed, [prices[x]['price'] for x in x_timed], label='30 minute price', color='blue')
|
2395
2376
|
s = ""
|
2396
|
-
for key in ['off_peak1', 'off_peak2']:
|
2397
|
-
if tariff['agile'].get(key) is not None and len(tariff['agile'][key]['
|
2398
|
-
p = tariff['agile'][key]
|
2399
|
-
plt.plot(x_timed, [p['
|
2400
|
-
s += f"\n {
|
2377
|
+
for key in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2378
|
+
if tariff['agile'].get(key) is not None and len(tariff['agile'][key]['slots']) > 0:
|
2379
|
+
p = tariff['agile'][key]
|
2380
|
+
plt.plot(x_timed, [p['avg'] if x in p['slots'] else None for x in x_timed], label=f"{key} {p['avg']:.1f}p")
|
2381
|
+
s += f"\n {hours_time(prices[p['slots'][0]]['start'])}-{hours_time(prices[p['slots'][-1]]['end'])} at {p['avg']:.1f}p"
|
2401
2382
|
output(f"\nCharge times{s}" if s != "" else "", 1)
|
2402
2383
|
plt.title(f"Pricing on {today} p/kWh inc VAT", fontsize=10)
|
2403
2384
|
plt.legend(fontsize=8)
|
@@ -2408,13 +2389,40 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2408
2389
|
# return the best charge time:
|
2409
2390
|
def get_best_charge_period(start, duration):
|
2410
2391
|
global tariff
|
2411
|
-
if tariff is None:
|
2412
|
-
return None
|
2413
|
-
key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3'] if hour_in(start, tariff.get(k))]
|
2414
|
-
|
2415
|
-
|
2416
|
-
|
2417
|
-
|
2392
|
+
if tariff is None or tariff.get('agile') is None or tariff['agile'].get('prices') is None:
|
2393
|
+
return None
|
2394
|
+
key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4'] if hour_in(start, tariff.get(k))]
|
2395
|
+
key = key[0] if len(key) > 0 else None
|
2396
|
+
end = tariff[key]['end'] if key is not None else round_time(start + duration)
|
2397
|
+
span = int(duration * 2 + 0.99)
|
2398
|
+
coverage = max([round_time(end - start), duration])
|
2399
|
+
period = {'start': start, 'end': round_time(start + coverage)}
|
2400
|
+
prices = tariff['agile']['prices']
|
2401
|
+
slots = [i for i in range(0, len(prices)) if hour_in(time_hours(prices[i]['start']), period)]
|
2402
|
+
if len(slots) == 0:
|
2403
|
+
return None
|
2404
|
+
elif len(slots) == 1:
|
2405
|
+
best = slots
|
2406
|
+
price = prices[slots[0]]['price']
|
2407
|
+
best_start = start
|
2408
|
+
else:
|
2409
|
+
# best charge time for duration
|
2410
|
+
weighting = tariff_config.get('weighting')
|
2411
|
+
times = []
|
2412
|
+
weights = ([1.0] * span) if weighting is None else (weighting + [1.0] * span)[:span]
|
2413
|
+
best = None
|
2414
|
+
price = None
|
2415
|
+
for i in range(0, len(slots) - span + 1):
|
2416
|
+
t = slots[i: i + span]
|
2417
|
+
p_span = [prices[x]['price'] for x in t]
|
2418
|
+
wavg = round(sum(p * w for p,w in zip(p_span, weights)) / sum(weights), 2)
|
2419
|
+
if price is None or wavg < price:
|
2420
|
+
price = wavg
|
2421
|
+
best = t
|
2422
|
+
best_start = prices[best[0]]['start']
|
2423
|
+
# save best time slot for charge duration
|
2424
|
+
tariff['agile']['best'] = {'start': best_start, 'end': round_time(best_start + span / 2), 'price': price, 'slots': best, 'key': key}
|
2425
|
+
return tariff['agile']['best']
|
2418
2426
|
|
2419
2427
|
# pushover app key for set_tariff()
|
2420
2428
|
set_tariff_app_key = "apx24dswzinhrbeb62sdensvt42aqe"
|
@@ -2458,7 +2466,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2458
2466
|
times = [times]
|
2459
2467
|
output(f"\n{use['name']}:")
|
2460
2468
|
for t in times:
|
2461
|
-
if len(t) not in (1,3,4) or t[0] not in ['off_peak1', 'off_peak2', 'off_peak3', 'peak1', 'peak2']:
|
2469
|
+
if len(t) not in (1,3,4) or t[0] not in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4', 'peak1', 'peak2']:
|
2462
2470
|
output(f"** set_tariff(): invalid time period {t}")
|
2463
2471
|
continue
|
2464
2472
|
key = t[0]
|
@@ -2497,7 +2505,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2497
2505
|
elif type(strategy) is not list:
|
2498
2506
|
strategy = [strategy]
|
2499
2507
|
output(f"\nStrategy")
|
2500
|
-
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3')])
|
2508
|
+
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3'), use.get('off_peak4')])
|
2501
2509
|
output_close(plot=tariff_config['show_plot'])
|
2502
2510
|
if update == 1:
|
2503
2511
|
tariff = use
|
@@ -2561,12 +2569,12 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2561
2569
|
while h < 48:
|
2562
2570
|
day = today if h < 24 else tomorrow
|
2563
2571
|
if forecast.daily.get(day) is None:
|
2564
|
-
value =
|
2572
|
+
value = None
|
2565
2573
|
elif steps_per_hour == 1:
|
2566
|
-
value =
|
2574
|
+
value = forecast.daily[day]['hourly'].get(int(h % 24))
|
2567
2575
|
else:
|
2568
|
-
value =
|
2569
|
-
profile.append(value)
|
2576
|
+
value = forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2))
|
2577
|
+
profile.append(c_float(value))
|
2570
2578
|
h += 1 / steps_per_hour
|
2571
2579
|
while len(profile) < run_time:
|
2572
2580
|
profile.append(0.0)
|
@@ -2652,7 +2660,7 @@ base_time = None
|
|
2652
2660
|
|
2653
2661
|
# charge_needed settings
|
2654
2662
|
charge_config = {
|
2655
|
-
'contingency': [
|
2663
|
+
'contingency': [15,10,5,10], # % of consumption. Single value or [winter, spring, summer, autumn]
|
2656
2664
|
'capacity': None, # Battery capacity (over-ride)
|
2657
2665
|
'min_soc': None, # Minimum Soc. Default 10%
|
2658
2666
|
'max_soc': None, # Maximum Soc. Default 100%
|
@@ -2691,7 +2699,7 @@ charge_config = {
|
|
2691
2699
|
'solcast': {'adjust': 0.95, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2},
|
2692
2700
|
'solar': {'adjust': 1.20, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2}
|
2693
2701
|
},
|
2694
|
-
'save': 'charge_needed
|
2702
|
+
'save': 'charge_needed ###.txt' # save calculation data for analysis
|
2695
2703
|
}
|
2696
2704
|
|
2697
2705
|
# app key for charge_needed (used to send output via pushover)
|
@@ -2706,10 +2714,10 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2706
2714
|
# forecast_times: list of hours when forecast can be fetched (UTC)
|
2707
2715
|
# force_charge: 1 = set force charge, 2 = charge for whole period
|
2708
2716
|
|
2709
|
-
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None,
|
2717
|
+
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2710
2718
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2711
2719
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2712
|
-
global timed_strategy, steps_per_hour, base_time
|
2720
|
+
global timed_strategy, steps_per_hour, base_time, storage
|
2713
2721
|
print(f"\n---------------- charge_needed ----------------")
|
2714
2722
|
# validate parameters
|
2715
2723
|
args = locals()
|
@@ -2758,17 +2766,20 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2758
2766
|
time_change = (change_hour - base_hour) * steps_per_hour
|
2759
2767
|
# get charge times
|
2760
2768
|
times = []
|
2761
|
-
for k in ['off_peak1', 'off_peak2', 'off_peak3']:
|
2769
|
+
for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2762
2770
|
if tariff is not None and tariff.get(k) is not None:
|
2763
|
-
start = time_hours(tariff[k]['start']) + (time_offset if tariff[k].get('gmt') is not None else 0)
|
2764
|
-
end = time_hours(tariff[k]['end']) + (time_offset if tariff[k].get('gmt') is not None else 0)
|
2771
|
+
start = round_time(time_hours(tariff[k]['start']) + (time_offset if tariff[k].get('gmt') is not None else 0))
|
2772
|
+
end = round_time(time_hours(tariff[k]['end']) + (time_offset if tariff[k].get('gmt') is not None else 0))
|
2765
2773
|
force = 0 if tariff[k].get('force') is not None and tariff[k]['force'] == 0 else force_charge
|
2766
2774
|
times.append({'key': k, 'start': start, 'end': end, 'force': force})
|
2767
2775
|
if len(times) == 0:
|
2768
|
-
times.append({'key': 'off_peak1', 'start': round_time(base_hour +
|
2769
|
-
output(f"Charge time: {hours_time(base_hour +
|
2776
|
+
times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'force': force_charge})
|
2777
|
+
output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
|
2770
2778
|
time_to_end1 = None
|
2771
2779
|
for t in times:
|
2780
|
+
if hour_in(hour_now, t) and update_settings > 0:
|
2781
|
+
update_settings = 0
|
2782
|
+
output(f"\nSettings will not be updated during a charge period")
|
2772
2783
|
time_to_start = round_time(t['start'] - base_hour) * steps_per_hour
|
2773
2784
|
time_to_start += hour_adjustment * steps_per_hour if time_to_start > time_change else 0
|
2774
2785
|
charge_time = round_time(t['end'] - t['start'])
|
@@ -2785,9 +2796,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2785
2796
|
time_to_start = times[0]['time_to_start']
|
2786
2797
|
time_to_end = times[0]['time_to_end']
|
2787
2798
|
charge_time = times[0]['charge_time']
|
2788
|
-
if hour_in(hour_now, {'start': round_time(start_at - 0.25), 'end': round_time(end_by + 0.25)}) and update_settings > 0:
|
2789
|
-
print(f"\nInverter settings will not be changed less than 15 minutes before or after the next charging period")
|
2790
|
-
update_settings = 0
|
2791
2799
|
# work out time window and times with clock changes
|
2792
2800
|
time_to_next = int(time_to_start)
|
2793
2801
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
@@ -2941,8 +2949,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2941
2949
|
output(f"\nConsumption (kWh):")
|
2942
2950
|
s = ""
|
2943
2951
|
for h in history:
|
2944
|
-
s += f"
|
2945
|
-
output(s)
|
2952
|
+
s += f" {h['date']} {h['total']:4.1f},"
|
2953
|
+
output(' ' + s[:-1])
|
2946
2954
|
output(f" Average of last {consumption_days} {day_tomorrow if consumption_span=='weekday' else 'day'}s: {consumption:.1f}kWh")
|
2947
2955
|
# time line buckets of consumption
|
2948
2956
|
daily_sum = sum(consumption_by_hour)
|
@@ -2951,15 +2959,12 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2951
2959
|
solcast_value = None
|
2952
2960
|
solcast_profile = None
|
2953
2961
|
if forecast is None and solcast_api_key is not None and solcast_api_key != 'my.solcast_api_key' and (system_time.hour in forecast_times or run_after == 0):
|
2954
|
-
fsolcast = Solcast(quiet=True,
|
2962
|
+
fsolcast = Solcast(quiet=True, reload=reload, shading=charge_config.get('shading'))
|
2955
2963
|
if fsolcast is not None and hasattr(fsolcast, 'daily') and fsolcast.daily.get(forecast_day) is not None:
|
2956
2964
|
solcast_value = fsolcast.daily[forecast_day]['kwh']
|
2957
2965
|
solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
|
2958
|
-
if
|
2959
|
-
|
2960
|
-
else:
|
2961
|
-
output(f"\nSolcast forecast for {forecast_day} = {solcast_value:.1f}kWh")
|
2962
|
-
# get forecast.solar data and produce time line
|
2966
|
+
solcast_from = time_hours(fsolcast.daily[today]['from']) if fsolcast.daily[today].get('from') is not None else 0
|
2967
|
+
output(f"\nSolcast forecast for {tomorrow}: {fsolcast.daily[tomorrow]['kwh']:.1f}") # get forecast.solar data and produce time line
|
2963
2968
|
solar_value = None
|
2964
2969
|
solar_profile = None
|
2965
2970
|
if forecast is None and solar_arrays is not None and (system_time.hour in forecast_times or run_after == 0):
|
@@ -2967,10 +2972,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2967
2972
|
if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
|
2968
2973
|
solar_value = fsolar.daily[forecast_day]['kwh']
|
2969
2974
|
solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
|
2970
|
-
|
2971
|
-
output(f"\nSolar forecast for {today} = {fsolar.daily[today]['kwh']:.1f}, {tomorrow} = {fsolar.daily[tomorrow]['kwh']:.1f}")
|
2972
|
-
else:
|
2973
|
-
output(f"\nSolar forecast for {forecast_day} = {solar_value:.1f}kWh")
|
2975
|
+
output(f"\nSolar forecast for {tomorrow}: {fsolar.daily[tomorrow]['kwh']:.1f}")
|
2974
2976
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2975
2977
|
output(f"\nNo forecasts available at this time")
|
2976
2978
|
# get generation data
|
@@ -2990,8 +2992,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2990
2992
|
output(f"\nGeneration (kWh):")
|
2991
2993
|
s = ""
|
2992
2994
|
for d in sorted(pv_history.keys())[-gen_days:]:
|
2993
|
-
s += f"
|
2994
|
-
output(s)
|
2995
|
+
s += f" {d} {pv_history[d]:4.1f},"
|
2996
|
+
output(' ' + s[:-1])
|
2995
2997
|
generation = pv_sum / gen_days
|
2996
2998
|
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
2997
2999
|
# choose expected value and produce generation time line
|
@@ -3070,24 +3072,23 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3070
3072
|
kwh_needed = test_charge
|
3071
3073
|
charge_message = "** test charge **"
|
3072
3074
|
# work out charge needed
|
3073
|
-
if kwh_min > reserve and kwh_needed < charge_config['min_kwh']:
|
3074
|
-
output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc
|
3075
|
+
if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh']:
|
3076
|
+
output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc:.0f}%)")
|
3075
3077
|
charge_message = "no charge needed"
|
3076
3078
|
kwh_needed = 0.0
|
3077
3079
|
hours = 0.0
|
3078
3080
|
start_timed = time_to_end
|
3079
3081
|
end_timed = time_to_end
|
3080
3082
|
end_soc = int(end_residual / capacity * 100 + 0.5)
|
3081
|
-
|
3082
|
-
# rebuild the battery residual with min_soc for battery hold
|
3083
|
+
# update min_soc for battery hold
|
3083
3084
|
if force_charge > 0 and timed_mode > 1:
|
3084
3085
|
for t in range(int(time_to_start), int(time_to_end)):
|
3085
3086
|
work_mode_timed[t]['min_soc'] = start_soc
|
3086
|
-
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
3087
3087
|
else:
|
3088
3088
|
if test_charge is None:
|
3089
|
-
output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc
|
3089
|
+
output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc:.0f}%)")
|
3090
3090
|
charge_message = "with charge added"
|
3091
|
+
output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
|
3091
3092
|
# work out time to add kwh_needed to battery
|
3092
3093
|
taper_time = 10/60 if (start_residual + kwh_needed) >= (capacity * 0.95) else 0
|
3093
3094
|
hours = round_time(kwh_needed / (charge_power * charge_loss) + taper_time)
|
@@ -3104,7 +3105,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3104
3105
|
price = charge_period.get('price') if charge_period is not None else None
|
3105
3106
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
3106
3107
|
end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
|
3107
|
-
output(f" Charge
|
3108
|
+
output(f" Charge: {hours_time(adjusted_hour(start_timed, time_line))}-{hours_time(adjusted_hour(end_timed, time_line))}" + (f" at {price:5.2f}p/kWh" if price is not None else ""))
|
3108
3109
|
for i in range(int(time_to_start), int(end_timed) + 1):
|
3109
3110
|
j = i + 1
|
3110
3111
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -3125,31 +3126,27 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3125
3126
|
work_mode_timed[i]['discharge'] *= (1-t)
|
3126
3127
|
elif force_charge > 0 and timed_mode > 1:
|
3127
3128
|
work_mode_timed[i]['min_soc'] = start_soc
|
3128
|
-
|
3129
|
-
|
3130
|
-
|
3131
|
-
|
3132
|
-
|
3133
|
-
|
3134
|
-
|
3135
|
-
|
3136
|
-
output(f"\nTime, Generation, Charge, Consumption, Discharge, Residual, kWh")
|
3137
|
-
for i in range(0, run_time):
|
3138
|
-
h = base_hour + i / steps_per_hour
|
3139
|
-
output(f" {hours_time(h)}, {generation_timed[i]:6.3f}, {charge_timed[i]:6.3f}, {consumption_timed[i]:6.3f}, {discharge_timed[i]:6.3f}, {bat_timed[i]:6.3f}")
|
3129
|
+
# rebuild the battery residual with the charge added and min_soc
|
3130
|
+
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
3131
|
+
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
3132
|
+
# show the results
|
3133
|
+
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
3134
|
+
output(f" Contingency: {kwh_contingency / capacity * 100:.0f}% SoC ({kwh_contingency:.2f}kWh)")
|
3135
|
+
if not charge_today:
|
3136
|
+
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
3140
3137
|
if show_data > 0:
|
3141
3138
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3142
|
-
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC
|
3139
|
+
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
|
3143
3140
|
h = base_hour + 1
|
3144
3141
|
t = steps_per_hour
|
3145
|
-
s += " " * (
|
3142
|
+
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3146
3143
|
while t < len(time_line) and bat_timed[t] is not None:
|
3147
3144
|
s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
|
3148
3145
|
s += f" {hours_time(time_line[t])}"
|
3149
|
-
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}
|
3146
|
+
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%,"
|
3150
3147
|
h += 1
|
3151
3148
|
t += steps_per_hour
|
3152
|
-
output(s)
|
3149
|
+
output(s[:-1])
|
3153
3150
|
if show_plot > 0:
|
3154
3151
|
print()
|
3155
3152
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -3190,7 +3187,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3190
3187
|
data['work_mode'] = work_mode_timed
|
3191
3188
|
data['generation'] = generation_timed
|
3192
3189
|
data['consumption'] = consumption_timed
|
3193
|
-
file = open(file_name, 'w')
|
3190
|
+
file = open(storage + file_name, 'w')
|
3194
3191
|
json.dump(data, file, sort_keys = True, indent=4, ensure_ascii= False)
|
3195
3192
|
file.close()
|
3196
3193
|
# setup charging
|
@@ -3216,13 +3213,13 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3216
3213
|
##################################################################################################
|
3217
3214
|
|
3218
3215
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3219
|
-
global charge_config
|
3216
|
+
global charge_config, storage
|
3220
3217
|
if save is None and charge_config.get('save') is not None:
|
3221
3218
|
save = charge_config.get('save').replace('###', base_time.replace(' ', 'T'))
|
3222
3219
|
if save is None:
|
3223
3220
|
print(f"** charge_compare(): please provide a saved file to load")
|
3224
3221
|
return
|
3225
|
-
file = open(save)
|
3222
|
+
file = open(storage + save)
|
3226
3223
|
data = json.load(file)
|
3227
3224
|
file.close()
|
3228
3225
|
if data is None or data.get('base_time') is None:
|
@@ -3242,7 +3239,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3242
3239
|
run_time = len(time_line)
|
3243
3240
|
base_hour = int(time_hours(base_time[11:16]))
|
3244
3241
|
start_day = base_time[:10]
|
3245
|
-
print(f"Run at {start_day} {hours_time(hour_now)} with SoC
|
3242
|
+
print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:.0f}%")
|
3246
3243
|
now = datetime.strptime(base_time, '%Y-%m-%d %H:%M')
|
3247
3244
|
end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
|
3248
3245
|
if v is None:
|
@@ -3277,17 +3274,17 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3277
3274
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3278
3275
|
if show_data > 0 and plots.get('SoC') is not None:
|
3279
3276
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3280
|
-
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC
|
3277
|
+
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
|
3281
3278
|
h = base_hour + 1
|
3282
3279
|
t = steps_per_hour
|
3283
|
-
s += " " * (
|
3280
|
+
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3284
3281
|
while t < len(time_line) and plots['SoC'][t] is not None:
|
3285
3282
|
s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
|
3286
3283
|
s += f" {hours_time(time_line[t])}"
|
3287
|
-
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}
|
3284
|
+
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%,"
|
3288
3285
|
h += 1
|
3289
3286
|
t += steps_per_hour
|
3290
|
-
print(s)
|
3287
|
+
print(s[:-1])
|
3291
3288
|
if show_plot > 0:
|
3292
3289
|
print()
|
3293
3290
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -3486,15 +3483,16 @@ def write(f, s, m='a'):
|
|
3486
3483
|
# log battery information in CSV format at 'interval' minutes apart for 'run' times
|
3487
3484
|
# log 1: battery info, 2: add cell volts, 3: add cell temps
|
3488
3485
|
def battery_monitor(interval=30, run=48, log=1, count=None, save=None, overwrite=0):
|
3486
|
+
global storage
|
3489
3487
|
run_time = interval * run / 60
|
3490
3488
|
print(f"\n---------------- battery_monitor ------------------")
|
3491
3489
|
print(f"Expected runtime = {hours_time(run_time, day=True)} (hh:mm/days)")
|
3492
3490
|
if save is not None:
|
3493
|
-
print(f"Saving data to {save} ")
|
3491
|
+
print(f"Saving data to {storage + save} ")
|
3494
3492
|
print()
|
3495
3493
|
s = f"time,soc,residual,bat_volt,bat_current,bat_temp,nbat,ncell,ntemp,volts*,imbalance*,temps*"
|
3496
3494
|
s += ",cell_volts*" if log == 2 else ",cell_volts*,cell_temps*" if log ==3 else ""
|
3497
|
-
write(save, s, 'w' if overwrite == 1 else 'a')
|
3495
|
+
write(storage + save, s, 'w' if overwrite == 1 else 'a')
|
3498
3496
|
i = run
|
3499
3497
|
while i > 0:
|
3500
3498
|
t1 = time.time()
|
@@ -3835,17 +3833,18 @@ class Solcast :
|
|
3835
3833
|
# reload: 0 = use solcast.json, 1 = load new forecast, 2 = use solcast.json if date matches
|
3836
3834
|
# The forecasts and estimated both include the current date, so the total number of days covered is 2 * days - 1.
|
3837
3835
|
# The forecasts and estimated also both include the current time, so the data has to be de-duplicated to get an accurate total for a day
|
3838
|
-
global debug_setting, solcast_url, solcast_api_key, solcast_save
|
3836
|
+
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3839
3837
|
self.data = {}
|
3840
3838
|
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3841
3839
|
self.today = datetime.strftime(datetime.date(datetime.now()), '%Y-%m-%d')
|
3840
|
+
self.quarter = int(self.today[5:7]) // 3 % 4
|
3842
3841
|
self.tomorrow = datetime.strftime(datetime.date(datetime.now() + timedelta(days=1)), '%Y-%m-%d')
|
3843
3842
|
self.yesterday = datetime.strftime(datetime.date(datetime.now() - timedelta(days=1)), '%Y-%m-%d')
|
3844
3843
|
self.save = solcast_save #.replace('.', '_%.'.replace('%', self.today.replace('-','')))
|
3845
|
-
if reload == 1 and os.path.exists(self.save):
|
3846
|
-
os.remove(self.save)
|
3847
|
-
if self.save is not None and os.path.exists(self.save):
|
3848
|
-
file = open(self.save)
|
3844
|
+
if reload == 1 and os.path.exists(storage + self.save):
|
3845
|
+
os.remove(storage + self.save)
|
3846
|
+
if self.save is not None and os.path.exists(storage + self.save):
|
3847
|
+
file = open(storage + self.save)
|
3849
3848
|
self.data = json.load(file)
|
3850
3849
|
file.close()
|
3851
3850
|
if len(self.data) == 0:
|
@@ -3886,7 +3885,7 @@ class Solcast :
|
|
3886
3885
|
return
|
3887
3886
|
self.data[t][rid] = response.json().get(t)
|
3888
3887
|
if self.save is not None :
|
3889
|
-
file = open(self.save, 'w')
|
3888
|
+
file = open(storage + self.save, 'w')
|
3890
3889
|
json.dump(self.data, file, sort_keys = True, indent=4, ensure_ascii= False)
|
3891
3890
|
file.close()
|
3892
3891
|
self.daily = {}
|
@@ -3918,26 +3917,30 @@ class Solcast :
|
|
3918
3917
|
while self.days > days * (1 + estimated) :
|
3919
3918
|
self.keys = self.keys[estimated:-1]
|
3920
3919
|
self.days = len(self.keys)
|
3921
|
-
# fill out forecast to cover 24 hours
|
3920
|
+
# fill out forecast to cover 24 hours and set forecast start time
|
3922
3921
|
for date in self.keys:
|
3923
3922
|
for t in [hours_time(t / 2) for t in range(0,48)]:
|
3924
3923
|
if self.daily[date]['pt30'].get(t) is None:
|
3925
3924
|
self.daily[date]['pt30'][t] = 0.0
|
3925
|
+
elif self.daily[date].get('from') is None:
|
3926
|
+
self.daily[date]['from'] = t
|
3926
3927
|
# apply shading
|
3927
3928
|
if self.shading is not None:
|
3928
3929
|
for date in self.keys:
|
3929
3930
|
times = sorted(time_hours(t) for t in self.daily[date]['pt30'].keys())
|
3930
3931
|
if self.shading.get('adjust') is not None:
|
3931
|
-
loss = self.shading['adjust']
|
3932
|
+
loss = self.shading['adjust'] if type(self.shading['adjust']) is not list else self.shading['adjust'][self.quarter]
|
3932
3933
|
for t in times:
|
3933
3934
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
3934
3935
|
if self.shading.get('am_delay') is not None:
|
3935
|
-
|
3936
|
+
delay = self.shading['am_delay'] if type(self.shading['am_delay']) is not list else self.shading['am_delay'][self.quarter]
|
3937
|
+
shaded = time_hours(self.daily[date]['sun'][0]) + delay
|
3936
3938
|
loss = self.shading['am_loss']
|
3937
3939
|
for t in [t for t in times if t < shaded]:
|
3938
3940
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
3939
3941
|
if self.shading.get('pm_delay') is not None:
|
3940
|
-
|
3942
|
+
delay = self.shading['pm_delay'] if type(self.shading['pm_delay']) is not list else self.shading['pm_delay'][self.quarter]
|
3943
|
+
shaded = time_hours(self.daily[date]['sun'][1]) - delay
|
3941
3944
|
loss = self.shading['pm_loss']
|
3942
3945
|
for t in [t for t in times if t > shaded]:
|
3943
3946
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
@@ -4173,19 +4176,19 @@ class Solar :
|
|
4173
4176
|
|
4174
4177
|
# get solar forecast and return total expected yield
|
4175
4178
|
def __init__(self, reload=0, quiet=False, shading=None):
|
4176
|
-
global solar_arrays, solar_save, solar_total, solar_url, solar_api_key
|
4177
|
-
self.shading = shading
|
4179
|
+
global solar_arrays, solar_save, solar_total, solar_url, solar_api_key, storage
|
4180
|
+
self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
|
4178
4181
|
self.today = datetime.strftime(datetime.date(datetime.now()), '%Y-%m-%d')
|
4182
|
+
self.quarter = int(self.today[5:7]) // 3 % 4
|
4179
4183
|
self.tomorrow = datetime.strftime(datetime.date(datetime.now() + timedelta(days=1)), '%Y-%m-%d')
|
4180
4184
|
self.yesterday = datetime.strftime(datetime.date(datetime.now() - timedelta(days=1)), '%Y-%m-%d')
|
4181
4185
|
self.arrays = None
|
4182
4186
|
self.results = None
|
4183
|
-
self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
|
4184
4187
|
self.save = solar_save #.replace('.', '_%.'.replace('%',self.today.replace('-','')))
|
4185
|
-
if reload == 1 and os.path.exists(self.save):
|
4186
|
-
os.remove(self.save)
|
4187
|
-
if self.save is not None and os.path.exists(self.save):
|
4188
|
-
file = open(self.save)
|
4188
|
+
if reload == 1 and os.path.exists(storage + self.save):
|
4189
|
+
os.remove(storage + self.save)
|
4190
|
+
if self.save is not None and os.path.exists(storage + self.save):
|
4191
|
+
file = open(storage + self.save)
|
4189
4192
|
data = json.load(file)
|
4190
4193
|
file.close()
|
4191
4194
|
if data.get('date') is not None and (data['date'] == self.today and reload != 1):
|
@@ -4216,7 +4219,7 @@ class Solar :
|
|
4216
4219
|
if self.save is not None :
|
4217
4220
|
if debug_setting > 0 and not quiet:
|
4218
4221
|
print(f"Saving data to {self.save}")
|
4219
|
-
file = open(self.save, 'w')
|
4222
|
+
file = open(storage + self.save, 'w')
|
4220
4223
|
json.dump({'date': self.today, 'arrays': self.arrays, 'results': self.results}, file, indent=4, ensure_ascii= False)
|
4221
4224
|
file.close()
|
4222
4225
|
self.daily = {}
|
@@ -4245,16 +4248,18 @@ class Solar :
|
|
4245
4248
|
for date in self.keys:
|
4246
4249
|
times = sorted(time_hours(t) for t in self.daily[date]['pt30'].keys())
|
4247
4250
|
if self.shading.get('adjust') is not None:
|
4248
|
-
loss = self.shading['adjust']
|
4251
|
+
loss = self.shading['adjust'] if type(self.shading['adjust']) is not list else self.shading['adjust'][self.quarter]
|
4249
4252
|
for t in times:
|
4250
4253
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
4251
4254
|
if self.shading.get('am_delay') is not None:
|
4252
|
-
|
4255
|
+
delay = self.shading['am_delay'] if type(self.shading['am_delay']) is not list else self.shading['am_delay'][self.quarter]
|
4256
|
+
shaded = time_hours(self.daily[date]['sun'][0]) + delay
|
4253
4257
|
loss = self.shading['am_loss']
|
4254
4258
|
for t in [t for t in times if t < shaded]:
|
4255
4259
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
4256
4260
|
if self.shading.get('pm_delay') is not None:
|
4257
|
-
|
4261
|
+
delay = self.shading['pm_delay'] if type(self.shading['pm_delay']) is not list else self.shading['pm_delay'][self.quarter]
|
4262
|
+
shaded = time_hours(self.daily[date]['sun'][1]) - delay
|
4258
4263
|
loss = self.shading['pm_loss']
|
4259
4264
|
for t in [t for t in times if t > shaded]:
|
4260
4265
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
@@ -4460,6 +4465,7 @@ class Solar :
|
|
4460
4465
|
plot_show()
|
4461
4466
|
return
|
4462
4467
|
|
4468
|
+
|
4463
4469
|
##################################################################################################
|
4464
4470
|
##################################################################################################
|
4465
4471
|
# Pushover API
|
@@ -4473,7 +4479,7 @@ pushover_url = "https://api.pushover.net/1/messages.json"
|
|
4473
4479
|
foxesscloud_app_key = "aqj8up6jeg9hu4zr1pgir3368vda4q"
|
4474
4480
|
|
4475
4481
|
def pushover_post(message, file=None, app_key=None):
|
4476
|
-
global pushover_user_key, pushover_url, foxesscloud_app_key, debug_setting
|
4482
|
+
global pushover_user_key, pushover_url, foxesscloud_app_key, debug_setting, storage
|
4477
4483
|
if pushover_user_key is None or message is None:
|
4478
4484
|
return None
|
4479
4485
|
if app_key is None:
|
@@ -4481,7 +4487,7 @@ def pushover_post(message, file=None, app_key=None):
|
|
4481
4487
|
if len(message) > 1024:
|
4482
4488
|
message = message[-1024:]
|
4483
4489
|
body = {'token': app_key, 'user': pushover_user_key, 'message': message}
|
4484
|
-
files = {'attachment': open(file, 'rb')} if file is not None else None
|
4490
|
+
files = {'attachment': open(storage + file, 'rb')} if file is not None else None
|
4485
4491
|
response = requests.post(pushover_url, data=body, files=files)
|
4486
4492
|
if response.status_code != 200:
|
4487
4493
|
print(f"** pushover_post() got response code {response.status_code}: {response.reason}")
|