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.
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud
4
- Updated: 17 September 2024
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.3"
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
- # best charge time for [0.5, 1, 1.5, 2 etc] hours
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 * 12
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]['times']) > 0:
2398
- p = tariff['agile'][key]['times'][-1]
2399
- plt.plot(x_timed, [p['price'] if x in p['best'] else None for x in x_timed], label=f"{key} {p['price']:.1f}p")
2400
- s += f"\n {format_period(p)} at {p['price']:.1f}p"
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))][0]
2414
- if tariff.get('agile') is None or tariff['agile'].get(key) is None:
2415
- return tariff.get(key)
2416
- i = min([int(duration * 2), len(tariff['agile'][key]['times']) - 1])
2417
- return tariff['agile'][key]['times'][i]
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 = 0.0
2572
+ value = None
2565
2573
  elif steps_per_hour == 1:
2566
- value = c_float(forecast.daily[day]['hourly'].get(int(h % 24)))
2574
+ value = forecast.daily[day]['hourly'].get(int(h % 24))
2567
2575
  else:
2568
- value = c_float(forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2)))
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': [20,10,5,15], # % of consumption. Single value or [winter, spring, summer, autumn]
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.txt' # save calculation data for analysis
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 + 2), 'end': round_time(base_hour + 5), 'force': force_charge})
2769
- output(f"Charge time: {hours_time(base_hour + 2)}-{hours_time(base_hour + 5)}")
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" {h['date']} {h['total']:4.1f}"
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, estimated=1 if charge_today else 0, shading=charge_config.get('shading'))
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 charge_today:
2959
- output(f"\nSolcast forecast for {today} = {fsolcast.daily[today]['kwh']:.1f}, {tomorrow} = {fsolcast.daily[tomorrow]['kwh']:.1f}")
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
- if charge_today:
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" {d} {pv_history[d]:4.1f}"
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}%)\n Forecast/Consumption: {expected:.1f}/{consumption:.1f} {expected / consumption * 100:.0f}%")
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
- output(f" Expected SoC at {hours_time(adjusted_hour(time_to_end, time_line))} is {end_soc}%")
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}%)\n Forecast/Consumption: {expected:.1f}/{consumption:.1f} {expected / consumption * 100:.0f}%")
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 {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
+ 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
- # rebuild the battery residual with the charge added and min_soc
3129
- (bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
3130
- end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
3131
- # show the state
3132
- output(f" Start SoC {start_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
3133
- output(f" End SoC {end_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
3134
- # show what we have worked out
3135
- if show_data == 3:
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 %:\n"
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 += " " * (13 if show_data == 2 else 12) * (h % data_wrap)
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 = {current_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 %:\n"
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 += " " * (13 if show_data == 2 else 12) * (h % data_wrap)
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
- shaded = time_hours(self.daily[date]['sun'][0]) + self.shading['am_delay']
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
- shaded = time_hours(self.daily[date]['sun'][1]) - self.shading['pm_delay']
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
- shaded = time_hours(self.daily[date]['sun'][0]) + self.shading['am_delay']
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
- shaded = time_hours(self.daily[date]['sun'][1]) - self.shading['pm_delay']
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}")