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/openapi.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
##################################################################################################
|
2
2
|
"""
|
3
3
|
Module: Fox ESS Cloud using Open API
|
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 2024
|
11
11
|
##################################################################################################
|
12
12
|
|
13
|
-
version = "2.5.
|
13
|
+
version = "2.5.3"
|
14
14
|
print(f"FoxESS-Cloud Open API version {version}")
|
15
15
|
|
16
16
|
debug_setting = 1
|
@@ -19,7 +19,6 @@ 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
|
-
|
23
22
|
import os.path
|
24
23
|
import json
|
25
24
|
import time
|
@@ -37,6 +36,9 @@ user_agent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTM
|
|
37
36
|
time_zone = 'Europe/London'
|
38
37
|
lang = 'en'
|
39
38
|
|
39
|
+
# optional path to use for file storage
|
40
|
+
storage = ''
|
41
|
+
|
40
42
|
# global plot parameters
|
41
43
|
figure_width = 9 # width of plots
|
42
44
|
legend_location = "upper right"
|
@@ -786,13 +788,15 @@ merge_settings = { # keys to add
|
|
786
788
|
'WorkMode': {'keys': {
|
787
789
|
'h115__': 'operation_mode__work_mode',
|
788
790
|
'h116__': 'operation_mode__work_mode',
|
789
|
-
'h117__': 'operation_mode__work_mode'
|
791
|
+
'h117__': 'operation_mode__work_mode',
|
792
|
+
# 'k106__': 'operation_mode__work_mode',
|
790
793
|
},
|
791
794
|
'values': ['SelfUse', 'Feedin', 'Backup']},
|
792
795
|
'BatteryVolt': {'keys': {
|
793
796
|
'h115__': ['h115__14', 'h115__15', 'h115__16'],
|
794
797
|
'h116__': ['h116__15', 'h116__16', 'h116__17'],
|
795
|
-
'h117__': ['h117__15', 'h117__16', 'h117__17']
|
798
|
+
'h117__': ['h117__15', 'h117__16', 'h117__17'],
|
799
|
+
# 'k106__': ['k106__xx', 'k106__xx', 'k106__xx'],
|
796
800
|
},
|
797
801
|
'type': 'list',
|
798
802
|
'valueType': 'float',
|
@@ -801,11 +805,11 @@ merge_settings = { # keys to add
|
|
801
805
|
'h115__': 'h115__17',
|
802
806
|
'h116__': 'h116__18',
|
803
807
|
'h117__': 'h117__18',
|
808
|
+
# 'k106__': 'k106__xx',
|
804
809
|
},
|
805
810
|
'type': 'list',
|
806
811
|
'valueType': 'int',
|
807
812
|
'unit': '℃'},
|
808
|
-
|
809
813
|
}
|
810
814
|
|
811
815
|
def get_ui():
|
@@ -1271,7 +1275,7 @@ sample_time = 5.0 # 5 minutes default
|
|
1271
1275
|
sample_rounding = 2 # round to 30 seconds
|
1272
1276
|
|
1273
1277
|
def get_history(time_span='hour', d=None, v=None, summary=1, save=None, load=None, plot=0):
|
1274
|
-
global token, device_sn, debug_setting, var_list, invert_ct2, tariff, max_power_kw, sample_rounding, sample_time, residual_scale
|
1278
|
+
global token, device_sn, debug_setting, var_list, invert_ct2, tariff, max_power_kw, sample_rounding, sample_time, residual_scale, storage
|
1275
1279
|
if get_device() is None:
|
1276
1280
|
return None
|
1277
1281
|
time_span = time_span.lower()
|
@@ -1316,12 +1320,12 @@ def get_history(time_span='hour', d=None, v=None, summary=1, save=None, load=Non
|
|
1316
1320
|
return None
|
1317
1321
|
result = result[0].get('datas')
|
1318
1322
|
else:
|
1319
|
-
file = open(load)
|
1323
|
+
file = open(storage + load)
|
1320
1324
|
result = json.load(file)
|
1321
1325
|
file.close()
|
1322
1326
|
if save is not None:
|
1323
1327
|
file_name = save + "_history_" + time_span + "_" + d[0:10].replace('-','') + ".txt"
|
1324
|
-
file = open(file_name, 'w', encoding='utf-8')
|
1328
|
+
file = open(storage + file_name, 'w', encoding='utf-8')
|
1325
1329
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1326
1330
|
file.close()
|
1327
1331
|
for var in result:
|
@@ -1391,7 +1395,7 @@ def get_history(time_span='hour', d=None, v=None, summary=1, save=None, load=Non
|
|
1391
1395
|
if e > 0.0:
|
1392
1396
|
kwh += e
|
1393
1397
|
if tariff is not None:
|
1394
|
-
if hour_in (h, [tariff.get('off_peak1'), tariff.get('off_peak2'), tariff.get('off_peak3')]):
|
1398
|
+
if hour_in (h, [tariff.get('off_peak1'), tariff.get('off_peak2'), tariff.get('off_peak3'), tariff.get('off_peak4')]):
|
1395
1399
|
kwh_off += e
|
1396
1400
|
elif hour_in(h, [tariff.get('peak1'), tariff.get('peak2')]):
|
1397
1401
|
kwh_peak += e
|
@@ -1537,7 +1541,7 @@ fix_value_threshold = 200000000.0
|
|
1537
1541
|
fix_value_mask = 0x0000FFFF
|
1538
1542
|
|
1539
1543
|
def get_report(dimension='day', d=None, v=None, summary=1, save=None, load=None, plot=0):
|
1540
|
-
global token, device_sn, var_list, debug_setting, report_vars
|
1544
|
+
global token, device_sn, var_list, debug_setting, report_vars, storage
|
1541
1545
|
if get_device() is None:
|
1542
1546
|
return None
|
1543
1547
|
# process list of days
|
@@ -1643,12 +1647,12 @@ def get_report(dimension='day', d=None, v=None, summary=1, save=None, load=None,
|
|
1643
1647
|
for x in v:
|
1644
1648
|
result.append({'variable': x, 'values': [], 'date': d})
|
1645
1649
|
if load is not None:
|
1646
|
-
file = open(load)
|
1650
|
+
file = open(storage + load)
|
1647
1651
|
result = json.load(file)
|
1648
1652
|
file.close()
|
1649
1653
|
elif save is not None:
|
1650
1654
|
file_name = save + "_report_" + dimension + "_" + d.replace('-','') + ".txt"
|
1651
|
-
file = open(file_name, 'w', encoding='utf-8')
|
1655
|
+
file = open(storage + file_name, 'w', encoding='utf-8')
|
1652
1656
|
json.dump(result, file, indent=4, ensure_ascii= False)
|
1653
1657
|
file.close()
|
1654
1658
|
if summary == 0:
|
@@ -2209,56 +2213,36 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2209
2213
|
strategy.append(prices[t])
|
2210
2214
|
output(f" {format_period(prices[t])} at {prices[t]['price']:.1f}p", 1)
|
2211
2215
|
tariff['agile']['strategy'] = strategy
|
2212
|
-
for key in ['off_peak1', 'off_peak2', 'off_peak3']:
|
2216
|
+
for key in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2213
2217
|
if tariff.get(key) is None:
|
2214
2218
|
continue
|
2215
2219
|
if tariff['agile'].get(key) is None:
|
2216
2220
|
tariff['agile'][key] = {}
|
2217
2221
|
# get price index for AM/PM charge times
|
2218
|
-
slots = []
|
2219
|
-
for i in range(0, len(prices)):
|
2220
|
-
if hour_in(time_hours(prices[i]['start']), tariff[key]):
|
2221
|
-
slots.append(i)
|
2222
|
+
slots = [i for i in range(0, len(prices)) if hour_in(time_hours(prices[i]['start']), tariff[key])]
|
2222
2223
|
tariff['agile'][key]['slots'] = slots
|
2223
|
-
|
2224
|
-
weighting = tariff_config.get('weighting')
|
2225
|
-
tariff['agile'][key]['times'] = []
|
2226
|
-
for j in range (0, len(slots)):
|
2227
|
-
span = j + 1
|
2228
|
-
weights = (([1.0] * (span-1) if weighting is None else weighting) + [0.5] * span)[:span]
|
2229
|
-
best = None
|
2230
|
-
price = None
|
2231
|
-
for i in range(0, len(slots) - j):
|
2232
|
-
t = slots[i: i + span]
|
2233
|
-
p_span = [prices[x]['price'] for x in t]
|
2234
|
-
wavg = round(sum(p * w for p,w in zip(p_span, weights)) / sum(weights), 2)
|
2235
|
-
if price is None or wavg < price:
|
2236
|
-
price = wavg
|
2237
|
-
best = t
|
2238
|
-
# save best time slot for charge duration
|
2239
|
-
start = prices[best[0]]['start']
|
2240
|
-
tariff['agile'][key]['times'].append({'start': start, 'end': round_time(start + span / 2), 'price': price, 'best': best, 'key': key})
|
2224
|
+
tariff['agile'][key]['avg'] = avg([prices[t]['price'] for t in slots])
|
2241
2225
|
# show the results
|
2242
2226
|
if tariff_config['show_data'] > 0:
|
2243
2227
|
data_wrap = tariff_config['data_wrap'] if tariff_config.get('data_wrap') is not None else 6
|
2244
2228
|
t = (now.hour * 2) % data_wrap
|
2245
|
-
s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t *
|
2229
|
+
s = f"\nPricing on {today} p/kWh inc VAT:\n" + " " * t * 13
|
2246
2230
|
for i in range(0, len(prices)):
|
2247
2231
|
s += "\n" if i > 0 and t % data_wrap == 0 else ""
|
2248
|
-
s += f" {prices[i]['time']} {prices[i]['price']:4.1f}"
|
2232
|
+
s += f" {prices[i]['time']} {prices[i]['price']:4.1f},"
|
2249
2233
|
t += 1
|
2250
|
-
output(s)
|
2234
|
+
output(s[:-1])
|
2251
2235
|
if tariff_config['show_plot'] > 0:
|
2252
2236
|
plt.figure(figsize=(figure_width, figure_width/2))
|
2253
2237
|
x_timed = [i for i in range(0, len(prices))]
|
2254
2238
|
plt.xticks(ticks=x_timed, labels=[prices[x]['time'] for x in x_timed], rotation=90, fontsize=8, ha='center')
|
2255
2239
|
plt.plot(x_timed, [prices[x]['price'] for x in x_timed], label='30 minute price', color='blue')
|
2256
2240
|
s = ""
|
2257
|
-
for key in ['off_peak1', 'off_peak2']:
|
2258
|
-
if tariff['agile'].get(key) is not None and len(tariff['agile'][key]['
|
2259
|
-
p = tariff['agile'][key]
|
2260
|
-
plt.plot(x_timed, [p['
|
2261
|
-
s += f"\n {
|
2241
|
+
for key in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2242
|
+
if tariff['agile'].get(key) is not None and len(tariff['agile'][key]['slots']) > 0:
|
2243
|
+
p = tariff['agile'][key]
|
2244
|
+
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")
|
2245
|
+
s += f"\n {hours_time(prices[p['slots'][0]]['start'])}-{hours_time(prices[p['slots'][-1]]['end'])} at {p['avg']:.1f}p"
|
2262
2246
|
output(f"\nCharge times{s}" if s != "" else "", 1)
|
2263
2247
|
plt.title(f"Pricing on {today} p/kWh inc VAT", fontsize=10)
|
2264
2248
|
plt.legend(fontsize=8)
|
@@ -2269,13 +2253,40 @@ def get_agile_times(tariff=agile_octopus, d=None):
|
|
2269
2253
|
# return the best charge time:
|
2270
2254
|
def get_best_charge_period(start, duration):
|
2271
2255
|
global tariff
|
2272
|
-
if tariff is None:
|
2256
|
+
if tariff is None or tariff.get('agile') is None or tariff['agile'].get('prices') is None:
|
2257
|
+
return None
|
2258
|
+
key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4'] if hour_in(start, tariff.get(k))]
|
2259
|
+
key = key[0] if len(key) > 0 else None
|
2260
|
+
end = tariff[key]['end'] if key is not None else round_time(start + duration)
|
2261
|
+
span = int(duration * 2 + 0.99)
|
2262
|
+
coverage = max([round_time(end - start), duration])
|
2263
|
+
period = {'start': start, 'end': round_time(start + coverage)}
|
2264
|
+
prices = tariff['agile']['prices']
|
2265
|
+
slots = [i for i in range(0, len(prices)) if hour_in(time_hours(prices[i]['start']), period)]
|
2266
|
+
if len(slots) == 0:
|
2273
2267
|
return None
|
2274
|
-
|
2275
|
-
|
2276
|
-
|
2277
|
-
|
2278
|
-
|
2268
|
+
elif len(slots) == 1:
|
2269
|
+
best = slots
|
2270
|
+
price = prices[slots[0]]['price']
|
2271
|
+
best_start = start
|
2272
|
+
else:
|
2273
|
+
# best charge time for duration
|
2274
|
+
weighting = tariff_config.get('weighting')
|
2275
|
+
times = []
|
2276
|
+
weights = ([1.0] * span) if weighting is None else (weighting + [1.0] * span)[:span]
|
2277
|
+
best = None
|
2278
|
+
price = None
|
2279
|
+
for i in range(0, len(slots) - span + 1):
|
2280
|
+
t = slots[i: i + span]
|
2281
|
+
p_span = [prices[x]['price'] for x in t]
|
2282
|
+
wavg = round(sum(p * w for p,w in zip(p_span, weights)) / sum(weights), 2)
|
2283
|
+
if price is None or wavg < price:
|
2284
|
+
price = wavg
|
2285
|
+
best = t
|
2286
|
+
best_start = prices[best[0]]['start']
|
2287
|
+
# save best time slot for charge duration
|
2288
|
+
tariff['agile']['best'] = {'start': best_start, 'end': round_time(best_start + span / 2), 'price': price, 'slots': best, 'key': key}
|
2289
|
+
return tariff['agile']['best']
|
2279
2290
|
|
2280
2291
|
# pushover app key for set_tariff()
|
2281
2292
|
set_tariff_app_key = "apx24dswzinhrbeb62sdensvt42aqe"
|
@@ -2319,7 +2330,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2319
2330
|
times = [times]
|
2320
2331
|
output(f"\n{use['name']}:")
|
2321
2332
|
for t in times:
|
2322
|
-
if len(t) not in (1,3,4) or t[0] not in ['off_peak1', 'off_peak2', 'off_peak3', 'peak1', 'peak2']:
|
2333
|
+
if len(t) not in (1,3,4) or t[0] not in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4', 'peak1', 'peak2']:
|
2323
2334
|
output(f"** set_tariff(): invalid time period {t}")
|
2324
2335
|
continue
|
2325
2336
|
key = t[0]
|
@@ -2358,7 +2369,7 @@ def set_tariff(find, update=1, times=None, forecast_times=None, strategy=None, d
|
|
2358
2369
|
elif type(strategy) is not list:
|
2359
2370
|
strategy = [strategy]
|
2360
2371
|
output(f"\nStrategy")
|
2361
|
-
use['strategy'] = get_strategy(use=use, strategy=strategy, quiet=0, remove=[use.get('off_peak1'), use.get('off_peak2'), use.get('off_peak3')])
|
2372
|
+
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')])
|
2362
2373
|
output_close(plot=tariff_config['show_plot'])
|
2363
2374
|
if update == 1:
|
2364
2375
|
tariff = use
|
@@ -2422,18 +2433,17 @@ def forecast_value_timed(forecast, today, tomorrow, base_hour, run_time, time_of
|
|
2422
2433
|
while h < 48:
|
2423
2434
|
day = today if h < 24 else tomorrow
|
2424
2435
|
if forecast.daily.get(day) is None:
|
2425
|
-
value =
|
2436
|
+
value = None
|
2426
2437
|
elif steps_per_hour == 1:
|
2427
|
-
value =
|
2438
|
+
value = forecast.daily[day]['hourly'].get(int(h % 24))
|
2428
2439
|
else:
|
2429
|
-
value =
|
2430
|
-
profile.append(value)
|
2440
|
+
value = forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2))
|
2441
|
+
profile.append(c_float(value))
|
2431
2442
|
h += 1 / steps_per_hour
|
2432
2443
|
while len(profile) < run_time:
|
2433
2444
|
profile.append(0.0)
|
2434
2445
|
return profile[:run_time]
|
2435
2446
|
|
2436
|
-
|
2437
2447
|
# build the timed work mode profile from the tariff strategy:
|
2438
2448
|
def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, current_mode=None):
|
2439
2449
|
global tariff, steps_per_hour
|
@@ -2514,7 +2524,7 @@ base_time = None
|
|
2514
2524
|
|
2515
2525
|
# charge_needed settings
|
2516
2526
|
charge_config = {
|
2517
|
-
'contingency': [
|
2527
|
+
'contingency': [15,10,5,10], # % of consumption. Single value or [winter, spring, summer, autumn]
|
2518
2528
|
'capacity': None, # Battery capacity (over-ride)
|
2519
2529
|
'min_soc': None, # Minimum Soc. Default 10%
|
2520
2530
|
'max_soc': None, # Maximum Soc. Default 100%
|
@@ -2553,7 +2563,7 @@ charge_config = {
|
|
2553
2563
|
'solcast': {'adjust': 0.95, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2},
|
2554
2564
|
'solar': {'adjust': 1.20, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2}
|
2555
2565
|
},
|
2556
|
-
'save': 'charge_needed
|
2566
|
+
'save': 'charge_needed ###.txt' # save calculation data for analysis
|
2557
2567
|
}
|
2558
2568
|
|
2559
2569
|
# app key for charge_needed (used to send output via pushover)
|
@@ -2568,10 +2578,10 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
|
|
2568
2578
|
# forecast_times: list of hours when forecast can be fetched (UTC)
|
2569
2579
|
# force_charge: 1 = set force charge, 2 = charge for whole period
|
2570
2580
|
|
2571
|
-
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None,
|
2581
|
+
def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
|
2572
2582
|
forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
|
2573
2583
|
global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
|
2574
|
-
global timed_strategy, steps_per_hour, base_time
|
2584
|
+
global timed_strategy, steps_per_hour, base_time, storage
|
2575
2585
|
print(f"\n---------------- charge_needed ----------------")
|
2576
2586
|
# validate parameters
|
2577
2587
|
args = locals()
|
@@ -2604,6 +2614,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2604
2614
|
now = system_time + timedelta(hours=time_offset)
|
2605
2615
|
today = datetime.strftime(now, '%Y-%m-%d')
|
2606
2616
|
base_hour = now.hour
|
2617
|
+
base_time = today + f" {hours_time(base_hour)}"
|
2607
2618
|
hour_now = now.hour + now.minute / 60
|
2608
2619
|
output(f" datetime = {today} {hours_time(hour_now)}", 2)
|
2609
2620
|
yesterday = datetime.strftime(now - timedelta(days=1), '%Y-%m-%d')
|
@@ -2619,17 +2630,20 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2619
2630
|
time_change = (change_hour - base_hour) * steps_per_hour
|
2620
2631
|
# get charge times
|
2621
2632
|
times = []
|
2622
|
-
for k in ['off_peak1', 'off_peak2', 'off_peak3']:
|
2633
|
+
for k in ['off_peak1', 'off_peak2', 'off_peak3', 'off_peak4']:
|
2623
2634
|
if tariff is not None and tariff.get(k) is not None:
|
2624
|
-
start = time_hours(tariff[k]['start']) + (time_offset if tariff[k].get('gmt') is not None else 0)
|
2625
|
-
end = time_hours(tariff[k]['end']) + (time_offset if tariff[k].get('gmt') is not None else 0)
|
2635
|
+
start = round_time(time_hours(tariff[k]['start']) + (time_offset if tariff[k].get('gmt') is not None else 0))
|
2636
|
+
end = round_time(time_hours(tariff[k]['end']) + (time_offset if tariff[k].get('gmt') is not None else 0))
|
2626
2637
|
force = 0 if tariff[k].get('force') is not None and tariff[k]['force'] == 0 else force_charge
|
2627
2638
|
times.append({'key': k, 'start': start, 'end': end, 'force': force})
|
2628
2639
|
if len(times) == 0:
|
2629
|
-
times.append({'key': 'off_peak1', 'start': round_time(base_hour +
|
2630
|
-
output(f"Charge time: {hours_time(base_hour +
|
2640
|
+
times.append({'key': 'off_peak1', 'start': round_time(base_hour + 1), 'end': round_time(base_hour + 4), 'force': force_charge})
|
2641
|
+
output(f"Charge time: {hours_time(base_hour + 1)}-{hours_time(base_hour + 4)}")
|
2631
2642
|
time_to_end1 = None
|
2632
2643
|
for t in times:
|
2644
|
+
if hour_in(hour_now, t) and update_settings > 0:
|
2645
|
+
update_settings = 0
|
2646
|
+
output(f"\nSettings will not be updated during a charge period")
|
2633
2647
|
time_to_start = round_time(t['start'] - base_hour) * steps_per_hour
|
2634
2648
|
time_to_start += hour_adjustment * steps_per_hour if time_to_start > time_change else 0
|
2635
2649
|
charge_time = round_time(t['end'] - t['start'])
|
@@ -2646,9 +2660,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2646
2660
|
time_to_start = times[0]['time_to_start']
|
2647
2661
|
time_to_end = times[0]['time_to_end']
|
2648
2662
|
charge_time = times[0]['charge_time']
|
2649
|
-
if hour_in(hour_now, {'start': round_time(start_at - 0.25), 'end': round_time(end_by + 0.25)}) and update_settings > 0:
|
2650
|
-
print(f"\nInverter settings will not be changed less than 15 minutes before or after the next charging period")
|
2651
|
-
update_settings = 0
|
2652
2663
|
# work out time window and times with clock changes
|
2653
2664
|
time_to_next = int(time_to_start)
|
2654
2665
|
charge_today = (base_hour + time_to_start / steps_per_hour) < 24
|
@@ -2802,8 +2813,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2802
2813
|
output(f"\nConsumption (kWh):")
|
2803
2814
|
s = ""
|
2804
2815
|
for h in history:
|
2805
|
-
s += f"
|
2806
|
-
output(s)
|
2816
|
+
s += f" {h['date']}: {h['total']:4.1f},"
|
2817
|
+
output(' ' + s[:-1])
|
2807
2818
|
output(f" Average of last {consumption_days} {day_tomorrow if consumption_span=='weekday' else 'day'}s: {consumption:.1f}kWh")
|
2808
2819
|
# time line buckets of consumption
|
2809
2820
|
daily_sum = sum(consumption_by_hour)
|
@@ -2812,14 +2823,11 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2812
2823
|
solcast_value = None
|
2813
2824
|
solcast_profile = None
|
2814
2825
|
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):
|
2815
|
-
fsolcast = Solcast(quiet=True,
|
2826
|
+
fsolcast = Solcast(quiet=True, reload=reload, shading=charge_config.get('shading'))
|
2816
2827
|
if fsolcast is not None and hasattr(fsolcast, 'daily') and fsolcast.daily.get(forecast_day) is not None:
|
2817
2828
|
solcast_value = fsolcast.daily[forecast_day]['kwh']
|
2818
2829
|
solcast_timed = forecast_value_timed(fsolcast, today, tomorrow, base_hour, run_time, time_offset)
|
2819
|
-
|
2820
|
-
output(f"\nSolcast forecast for {today} = {fsolcast.daily[today]['kwh']:.1f}, {tomorrow} = {fsolcast.daily[tomorrow]['kwh']:.1f}")
|
2821
|
-
else:
|
2822
|
-
output(f"\nSolcast forecast for {forecast_day} = {solcast_value:.1f}kWh")
|
2830
|
+
output(f"\nSolcast forecast for {tomorrow}: {fsolcast.daily[tomorrow]['kwh']:.1f}")
|
2823
2831
|
# get forecast.solar data and produce time line
|
2824
2832
|
solar_value = None
|
2825
2833
|
solar_profile = None
|
@@ -2828,10 +2836,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2828
2836
|
if fsolar is not None and hasattr(fsolar, 'daily') and fsolar.daily.get(forecast_day) is not None:
|
2829
2837
|
solar_value = fsolar.daily[forecast_day]['kwh']
|
2830
2838
|
solar_timed = forecast_value_timed(fsolar, today, tomorrow, base_hour, run_time, 0)
|
2831
|
-
|
2832
|
-
output(f"\nSolar forecast for {today} = {fsolar.daily[today]['kwh']:.1f}, {tomorrow} = {fsolar.daily[tomorrow]['kwh']:.1f}")
|
2833
|
-
else:
|
2834
|
-
output(f"\nSolar forecast for {forecast_day} = {solar_value:.1f}kWh")
|
2839
|
+
output(f"\nSolar forecast for {tomorrow}: {fsolar.daily[tomorrow]['kwh']:.1f}")
|
2835
2840
|
if solcast_value is None and solar_value is None and debug_setting > 1:
|
2836
2841
|
output(f"\nNo forecasts available at this time")
|
2837
2842
|
# get generation data
|
@@ -2851,8 +2856,8 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2851
2856
|
output(f"\nGeneration (kWh):")
|
2852
2857
|
s = ""
|
2853
2858
|
for d in sorted(pv_history.keys())[-gen_days:]:
|
2854
|
-
s += f"
|
2855
|
-
output(s)
|
2859
|
+
s += f" {d}: {pv_history[d]:4.1f},"
|
2860
|
+
output(' ' + s[:-1])
|
2856
2861
|
generation = pv_sum / gen_days
|
2857
2862
|
output(f" Average of last {gen_days} days: {generation:.1f}kWh")
|
2858
2863
|
# choose expected value and produce generation time line
|
@@ -2931,24 +2936,23 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2931
2936
|
kwh_needed = test_charge
|
2932
2937
|
charge_message = "** test charge **"
|
2933
2938
|
# work out charge needed
|
2934
|
-
if kwh_min > reserve and kwh_needed < charge_config['min_kwh'] and full_charge is None and test_charge is None and force_charge != 2:
|
2935
|
-
output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc
|
2939
|
+
if kwh_min > (reserve + kwh_contingency) and kwh_needed < charge_config['min_kwh'] and full_charge is None and test_charge is None and force_charge != 2:
|
2940
|
+
output(f"\nNo charging needed ({today} {hours_time(hour_now)} {current_soc:.0f}%)")
|
2936
2941
|
charge_message = "no charge needed"
|
2937
2942
|
kwh_needed = 0.0
|
2938
2943
|
hours = 0.0
|
2939
2944
|
start_timed = time_to_end
|
2940
2945
|
end_timed = time_to_end
|
2941
2946
|
end_soc = int(end_residual / capacity * 100 + 0.5)
|
2942
|
-
|
2943
|
-
# rebuild the battery residual with min_soc for battery hold
|
2947
|
+
# update min_soc for battery hold
|
2944
2948
|
if force_charge > 0 and timed_mode > 1:
|
2945
2949
|
for t in range(int(time_to_start), int(time_to_end)):
|
2946
2950
|
work_mode_timed[t]['min_soc'] = start_soc
|
2947
|
-
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
2948
2951
|
else:
|
2949
2952
|
if test_charge is None:
|
2950
|
-
output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc
|
2953
|
+
output(f"\nCharge needed {kwh_needed:.2f}kWh ({today} {hours_time(hour_now)} {current_soc:.0f}%)")
|
2951
2954
|
charge_message = "with charge added"
|
2955
|
+
output(f" Start SoC: {start_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
|
2952
2956
|
# work out time to add kwh_needed to battery
|
2953
2957
|
taper_time = 10/60 if (start_residual + kwh_needed) >= (capacity * 0.95) else 0
|
2954
2958
|
hours = round_time(kwh_needed / (charge_power * charge_loss) + taper_time)
|
@@ -2965,7 +2969,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2965
2969
|
price = charge_period.get('price') if charge_period is not None else None
|
2966
2970
|
start_timed = time_to_start + charge_offset * steps_per_hour
|
2967
2971
|
end_timed = (start_timed + hours * steps_per_hour) if force_charge == 0 else time_to_end
|
2968
|
-
output(f" Charge
|
2972
|
+
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 ""))
|
2969
2973
|
for i in range(int(time_to_start), int(end_timed) + 1):
|
2970
2974
|
j = i + 1
|
2971
2975
|
# work out time (fraction of hour) when charging in hour from i to j
|
@@ -2986,31 +2990,27 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
2986
2990
|
work_mode_timed[i]['discharge'] *= (1-t)
|
2987
2991
|
elif force_charge > 0 and timed_mode > 1:
|
2988
2992
|
work_mode_timed[i]['min_soc'] = start_soc
|
2989
|
-
|
2990
|
-
|
2991
|
-
|
2992
|
-
|
2993
|
-
|
2994
|
-
|
2995
|
-
|
2996
|
-
|
2997
|
-
output(f"\nTime, Generation, Charge, Consumption, Discharge, Residual, kWh")
|
2998
|
-
for i in range(0, run_time):
|
2999
|
-
h = base_hour + i / steps_per_hour
|
3000
|
-
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}")
|
2993
|
+
# rebuild the battery residual with any charge added and min_soc
|
2994
|
+
(bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
|
2995
|
+
end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
|
2996
|
+
# show the results
|
2997
|
+
output(f" End SoC: {end_residual / capacity * 100:.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
|
2998
|
+
output(f" Contingency: {kwh_contingency / capacity * 100:.0f}% SoC ({kwh_contingency:.2f}kWh)")
|
2999
|
+
if not charge_today:
|
3000
|
+
output(f" PV cover: {expected / consumption * 100:.0f}% ({expected:.1f}/{consumption:.1f})")
|
3001
3001
|
if show_data > 0:
|
3002
3002
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3003
|
-
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC
|
3003
|
+
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
|
3004
3004
|
h = base_hour + 1
|
3005
3005
|
t = steps_per_hour
|
3006
|
-
s += " " * (
|
3006
|
+
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3007
3007
|
while t < len(time_line) and bat_timed[t] is not None:
|
3008
3008
|
s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
|
3009
3009
|
s += f" {hours_time(time_line[t])}"
|
3010
|
-
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}
|
3010
|
+
s += f" {bat_timed[t]:5.2f}" if show_data == 2 else f" {bat_timed[t] / capacity * 100:3.0f}%,"
|
3011
3011
|
h += 1
|
3012
3012
|
t += steps_per_hour
|
3013
|
-
output(s)
|
3013
|
+
output(s[:-1])
|
3014
3014
|
if show_plot > 0:
|
3015
3015
|
print()
|
3016
3016
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -3051,7 +3051,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3051
3051
|
data['work_mode'] = work_mode_timed
|
3052
3052
|
data['generation'] = generation_timed
|
3053
3053
|
data['consumption'] = consumption_timed
|
3054
|
-
file = open(file_name, 'w')
|
3054
|
+
file = open(storage + file_name, 'w')
|
3055
3055
|
json.dump(data, file, sort_keys = True, indent=4, ensure_ascii= False)
|
3056
3056
|
file.close()
|
3057
3057
|
# setup charging
|
@@ -3076,13 +3076,13 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
|
|
3076
3076
|
##################################################################################################
|
3077
3077
|
|
3078
3078
|
def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
3079
|
-
global charge_config
|
3079
|
+
global charge_config, storage
|
3080
3080
|
if save is None and charge_config.get('save') is not None:
|
3081
3081
|
save = charge_config.get('save').replace('###', base_time.replace(' ', 'T'))
|
3082
3082
|
if save is None:
|
3083
3083
|
print(f"** charge_compare(): please provide a saved file to load")
|
3084
3084
|
return
|
3085
|
-
file = open(save)
|
3085
|
+
file = open(storage + save)
|
3086
3086
|
data = json.load(file)
|
3087
3087
|
file.close()
|
3088
3088
|
if data is None or data.get('base_time') is None:
|
@@ -3102,7 +3102,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3102
3102
|
run_time = len(time_line)
|
3103
3103
|
base_hour = int(time_hours(base_time[11:16]))
|
3104
3104
|
start_day = base_time[:10]
|
3105
|
-
print(f"Run at {start_day} {hours_time(hour_now)} with SoC
|
3105
|
+
print(f"Run at {start_day} {hours_time(hour_now)} with SoC {current_soc:.0f}%")
|
3106
3106
|
now = datetime.strptime(base_time, '%Y-%m-%d %H:%M')
|
3107
3107
|
end_day = datetime.strftime(now + timedelta(hours=run_time / steps_per_hour), '%Y-%m-%d')
|
3108
3108
|
if v is None:
|
@@ -3137,17 +3137,17 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
|
|
3137
3137
|
plots[v][i] = plots[v][i] / count[v][i] if count[v][i] > 0 else None
|
3138
3138
|
if show_data > 0 and plots.get('SoC') is not None:
|
3139
3139
|
data_wrap = charge_config['data_wrap'] if charge_config.get('data_wrap') is not None else 6
|
3140
|
-
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC
|
3140
|
+
s = f"\nBattery Energy kWh:\n" if show_data == 2 else f"\nBattery SoC:\n"
|
3141
3141
|
h = base_hour + 1
|
3142
3142
|
t = steps_per_hour
|
3143
|
-
s += " " * (
|
3143
|
+
s += " " * (14 if show_data == 2 else 13) * (h % data_wrap)
|
3144
3144
|
while t < len(time_line) and plots['SoC'][t] is not None:
|
3145
3145
|
s += "\n" if t > steps_per_hour and h % data_wrap == 0 else ""
|
3146
3146
|
s += f" {hours_time(time_line[t])}"
|
3147
|
-
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}
|
3147
|
+
s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%,"
|
3148
3148
|
h += 1
|
3149
3149
|
t += steps_per_hour
|
3150
|
-
print(s)
|
3150
|
+
print(s[:-1])
|
3151
3151
|
if show_plot > 0:
|
3152
3152
|
print()
|
3153
3153
|
plt.figure(figsize=(figure_width, figure_width/2))
|
@@ -3345,6 +3345,7 @@ def write(f, s, m='a'):
|
|
3345
3345
|
# log battery information in CSV format at 'interval' minutes apart for 'run' times
|
3346
3346
|
# log 1: battery info, 2: add cell volts, 3: add cell temps
|
3347
3347
|
def battery_monitor(interval=30, run=48, log=1, count=None, save=None, overwrite=0):
|
3348
|
+
global storage
|
3348
3349
|
run_time = interval * run / 60
|
3349
3350
|
print(f"\n---------------- battery_monitor ------------------")
|
3350
3351
|
print(f"Expected runtime = {hours_time(run_time, day=True)} (hh:mm/days)")
|
@@ -3353,7 +3354,7 @@ def battery_monitor(interval=30, run=48, log=1, count=None, save=None, overwrite
|
|
3353
3354
|
print()
|
3354
3355
|
s = f"time,soc,residual,bat_volt,bat_current,bat_temp,nbat,ncell,ntemp,volts*,imbalance*,temps*"
|
3355
3356
|
s += ",cell_volts*" if log == 2 else ",cell_volts*,cell_temps*" if log ==3 else ""
|
3356
|
-
write(save, s, 'w' if overwrite == 1 else 'a')
|
3357
|
+
write(storage + save, s, 'w' if overwrite == 1 else 'a')
|
3357
3358
|
i = run
|
3358
3359
|
while i > 0:
|
3359
3360
|
t1 = time.time()
|
@@ -3694,17 +3695,18 @@ class Solcast :
|
|
3694
3695
|
# reload: 0 = use solcast.json, 1 = load new forecast, 2 = use solcast.json if date matches
|
3695
3696
|
# The forecasts and estimated both include the current date, so the total number of days covered is 2 * days - 1.
|
3696
3697
|
# 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
|
3697
|
-
global debug_setting, solcast_url, solcast_api_key, solcast_save
|
3698
|
+
global debug_setting, solcast_url, solcast_api_key, solcast_save, storage
|
3698
3699
|
self.data = {}
|
3699
|
-
self.shading = shading
|
3700
|
+
self.shading = None if shading is None else shading if shading.get('solcast') is None else shading['solcast']
|
3700
3701
|
self.today = datetime.strftime(datetime.date(datetime.now()), '%Y-%m-%d')
|
3702
|
+
self.quarter = int(self.today[5:7]) // 3 % 4
|
3701
3703
|
self.tomorrow = datetime.strftime(datetime.date(datetime.now() + timedelta(days=1)), '%Y-%m-%d')
|
3702
3704
|
self.yesterday = datetime.strftime(datetime.date(datetime.now() - timedelta(days=1)), '%Y-%m-%d')
|
3703
3705
|
self.save = solcast_save #.replace('.', '_%.'.replace('%', self.today.replace('-','')))
|
3704
|
-
if reload == 1 and os.path.exists(self.save):
|
3705
|
-
os.remove(self.save)
|
3706
|
-
if self.save is not None and os.path.exists(self.save):
|
3707
|
-
file = open(self.save)
|
3706
|
+
if reload == 1 and os.path.exists(storage + self.save):
|
3707
|
+
os.remove(storage + self.save)
|
3708
|
+
if self.save is not None and os.path.exists(storage + self.save):
|
3709
|
+
file = open(storage + self.save)
|
3708
3710
|
self.data = json.load(file)
|
3709
3711
|
file.close()
|
3710
3712
|
if len(self.data) == 0:
|
@@ -3745,12 +3747,13 @@ class Solcast :
|
|
3745
3747
|
return
|
3746
3748
|
self.data[t][rid] = response.json().get(t)
|
3747
3749
|
if self.save is not None :
|
3748
|
-
file = open(self.save, 'w')
|
3750
|
+
file = open(storage + self.save, 'w')
|
3749
3751
|
json.dump(self.data, file, sort_keys = True, indent=4, ensure_ascii= False)
|
3750
3752
|
file.close()
|
3751
3753
|
self.daily = {}
|
3754
|
+
estimated = 0 if self.data.get('estimated_actuals') is None else 1
|
3752
3755
|
loaded = {} # track what we have loaded so we don't duplicate between forecast and actuals
|
3753
|
-
for t in ['forecasts'] if
|
3756
|
+
for t in ['forecasts'] if estimated == 0 else ['forecasts', 'estimated_actuals']:
|
3754
3757
|
for rid in self.data[t].keys() : # aggregate sites
|
3755
3758
|
if loaded.get(rid) is None:
|
3756
3759
|
loaded[rid] = {}
|
@@ -3770,33 +3773,37 @@ class Solcast :
|
|
3770
3773
|
self.daily[date]['pt30'][key] = 0.0
|
3771
3774
|
self.daily [date]['pt30'][key] += value
|
3772
3775
|
# ignore first and last dates as these only cover part of the day, so are not accurate
|
3773
|
-
self.keys = sorted(self.daily.keys())[
|
3776
|
+
self.keys = sorted(self.daily.keys())[estimated:-1]
|
3774
3777
|
self.days = len(self.keys)
|
3775
3778
|
# trim the range if fewer days have been requested
|
3776
|
-
while self.days >
|
3777
|
-
self.keys = self.keys[
|
3779
|
+
while self.days > days * (1 + estimated) :
|
3780
|
+
self.keys = self.keys[estimated:-1]
|
3778
3781
|
self.days = len(self.keys)
|
3779
|
-
# fill out forecast to cover 24 hours
|
3782
|
+
# fill out forecast to cover 24 hours and set forecast start time
|
3780
3783
|
for date in self.keys:
|
3781
3784
|
for t in [hours_time(t / 2) for t in range(0,48)]:
|
3782
3785
|
if self.daily[date]['pt30'].get(t) is None:
|
3783
3786
|
self.daily[date]['pt30'][t] = 0.0
|
3787
|
+
elif self.daily[date].get('from') is None:
|
3788
|
+
self.daily[date]['from'] = t
|
3784
3789
|
# apply shading
|
3785
|
-
if self.shading is not None
|
3790
|
+
if self.shading is not None:
|
3786
3791
|
for date in self.keys:
|
3787
3792
|
times = sorted(time_hours(t) for t in self.daily[date]['pt30'].keys())
|
3788
|
-
if self.shading
|
3789
|
-
loss = self.shading['
|
3793
|
+
if self.shading.get('adjust') is not None:
|
3794
|
+
loss = self.shading['adjust'] if type(self.shading['adjust']) is not list else self.shading['adjust'][self.quarter]
|
3790
3795
|
for t in times:
|
3791
3796
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
3792
|
-
if self.shading
|
3793
|
-
|
3794
|
-
|
3797
|
+
if self.shading.get('am_delay') is not None:
|
3798
|
+
delay = self.shading['am_delay'] if type(self.shading['am_delay']) is not list else self.shading['am_delay'][self.quarter]
|
3799
|
+
shaded = time_hours(self.daily[date]['sun'][0]) + delay
|
3800
|
+
loss = self.shading['am_loss']
|
3795
3801
|
for t in [t for t in times if t < shaded]:
|
3796
3802
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
3797
|
-
if self.shading
|
3798
|
-
|
3799
|
-
|
3803
|
+
if self.shading.get('pm_delay') is not None:
|
3804
|
+
delay = self.shading['pm_delay'] if type(self.shading['pm_delay']) is not list else self.shading['pm_delay'][self.quarter]
|
3805
|
+
shaded = time_hours(self.daily[date]['sun'][1]) - delay
|
3806
|
+
loss = self.shading['pm_loss']
|
3800
3807
|
for t in [t for t in times if t > shaded]:
|
3801
3808
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
3802
3809
|
# calculate hourly values and total
|
@@ -4031,18 +4038,19 @@ class Solar :
|
|
4031
4038
|
|
4032
4039
|
# get solar forecast and return total expected yield
|
4033
4040
|
def __init__(self, reload=0, quiet=False, shading=None):
|
4034
|
-
global solar_arrays, solar_save, solar_total, solar_url, solar_api_key
|
4035
|
-
self.shading = shading
|
4041
|
+
global solar_arrays, solar_save, solar_total, solar_url, solar_api_key, storage
|
4042
|
+
self.shading = None if shading is None else shading if shading.get('solar') is None else shading['solar']
|
4036
4043
|
self.today = datetime.strftime(datetime.date(datetime.now()), '%Y-%m-%d')
|
4044
|
+
self.quarter = int(self.today[5:7]) // 3 % 4
|
4037
4045
|
self.tomorrow = datetime.strftime(datetime.date(datetime.now() + timedelta(days=1)), '%Y-%m-%d')
|
4046
|
+
self.yesterday = datetime.strftime(datetime.date(datetime.now() - timedelta(days=1)), '%Y-%m-%d')
|
4038
4047
|
self.arrays = None
|
4039
4048
|
self.results = None
|
4040
|
-
self.shading = shading
|
4041
4049
|
self.save = solar_save #.replace('.', '_%.'.replace('%',self.today.replace('-','')))
|
4042
|
-
if reload == 1 and os.path.exists(self.save):
|
4043
|
-
os.remove(self.save)
|
4044
|
-
if self.save is not None and os.path.exists(self.save):
|
4045
|
-
file = open(self.save)
|
4050
|
+
if reload == 1 and os.path.exists(storage + self.save):
|
4051
|
+
os.remove(storage + self.save)
|
4052
|
+
if self.save is not None and os.path.exists(storage + self.save):
|
4053
|
+
file = open(storage + self.save)
|
4046
4054
|
data = json.load(file)
|
4047
4055
|
file.close()
|
4048
4056
|
if data.get('date') is not None and (data['date'] == self.today and reload != 1):
|
@@ -4073,7 +4081,7 @@ class Solar :
|
|
4073
4081
|
if self.save is not None :
|
4074
4082
|
if debug_setting > 0 and not quiet:
|
4075
4083
|
print(f"Saving data to {self.save}")
|
4076
|
-
file = open(self.save, 'w')
|
4084
|
+
file = open(storage + self.save, 'w')
|
4077
4085
|
json.dump({'date': self.today, 'arrays': self.arrays, 'results': self.results}, file, indent=4, ensure_ascii= False)
|
4078
4086
|
file.close()
|
4079
4087
|
self.daily = {}
|
@@ -4101,18 +4109,20 @@ class Solar :
|
|
4101
4109
|
if self.shading is not None and self.shading.get('solar') is not None:
|
4102
4110
|
for date in self.keys:
|
4103
4111
|
times = sorted(time_hours(t) for t in self.daily[date]['pt30'].keys())
|
4104
|
-
if self.shading
|
4105
|
-
loss = self.shading['
|
4112
|
+
if self.shading.get('adjust') is not None:
|
4113
|
+
loss = self.shading['adjust'] if type(self.shading['adjust']) is not list else self.shading['adjust'][self.quarter]
|
4106
4114
|
for t in times:
|
4107
4115
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
4108
|
-
if self.shading
|
4109
|
-
|
4110
|
-
|
4116
|
+
if self.shading.get('am_delay') is not None:
|
4117
|
+
delay = self.shading['am_delay'] if type(self.shading['am_delay']) is not list else self.shading['am_delay'][self.quarter]
|
4118
|
+
shaded = time_hours(self.daily[date]['sun'][0]) + delay
|
4119
|
+
loss = self.shading['am_loss']
|
4111
4120
|
for t in [t for t in times if t < shaded]:
|
4112
4121
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
4113
|
-
if self.shading
|
4114
|
-
|
4115
|
-
|
4122
|
+
if self.shading.get('pm_delay') is not None:
|
4123
|
+
delay = self.shading['pm_delay'] if type(self.shading['pm_delay']) is not list else self.shading['pm_delay'][self.quarter]
|
4124
|
+
shaded = time_hours(self.daily[date]['sun'][1]) - delay
|
4125
|
+
loss = self.shading['pm_loss']
|
4116
4126
|
for t in [t for t in times if t > shaded]:
|
4117
4127
|
self.daily[date]['pt30'][hours_time(t)] *= loss
|
4118
4128
|
# calculate hourly values and total
|
@@ -4331,7 +4341,7 @@ pushover_url = "https://api.pushover.net/1/messages.json"
|
|
4331
4341
|
foxesscloud_app_key = "aqj8up6jeg9hu4zr1pgir3368vda4q"
|
4332
4342
|
|
4333
4343
|
def pushover_post(message, file=None, app_key=None):
|
4334
|
-
global pushover_user_key, pushover_url, foxesscloud_app_key, debug_setting
|
4344
|
+
global pushover_user_key, pushover_url, foxesscloud_app_key, debug_setting, storage
|
4335
4345
|
if pushover_user_key is None or message is None:
|
4336
4346
|
return None
|
4337
4347
|
if app_key is None:
|
@@ -4339,7 +4349,7 @@ def pushover_post(message, file=None, app_key=None):
|
|
4339
4349
|
if len(message) > 1024:
|
4340
4350
|
message = message[-1024:]
|
4341
4351
|
body = {'token': app_key, 'user': pushover_user_key, 'message': message}
|
4342
|
-
files = {'attachment': open(file, 'rb')} if file is not None else None
|
4352
|
+
files = {'attachment': open(storage + file, 'rb')} if file is not None else None
|
4343
4353
|
response = requests.post(pushover_url, data=body, files=files)
|
4344
4354
|
if response.status_code != 200:
|
4345
4355
|
print(f"** pushover_post() got response code {response.status_code}: {response.reason}")
|