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/openapi.py CHANGED
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
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 2024
11
11
  ##################################################################################################
12
12
 
13
- version = "2.5.1"
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
- # best charge time for [0.5, 1, 1.5, 2 etc] hours
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 * 12
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]['times']) > 0:
2259
- p = tariff['agile'][key]['times'][-1]
2260
- 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")
2261
- s += f"\n {format_period(p)} at {p['price']:.1f}p"
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
- key = [k for k in ['off_peak1', 'off_peak2', 'off_peak3'] if hour_in(start, tariff.get(k))][0]
2275
- if tariff.get('agile') is None or tariff['agile'].get(key) is None:
2276
- return tariff.get(key)
2277
- i = min([int(duration * 2), len(tariff['agile'][key]['times']) - 1])
2278
- return tariff['agile'][key]['times'][i]
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 = 0.0
2436
+ value = None
2426
2437
  elif steps_per_hour == 1:
2427
- value = c_float(forecast.daily[day]['hourly'].get(int(h % 24)))
2438
+ value = forecast.daily[day]['hourly'].get(int(h % 24))
2428
2439
  else:
2429
- value = c_float(forecast.daily[day]['pt30'].get(hours_time(int(h * 2) / 2)))
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': [20,10,5,15], # % of consumption. Single value or [winter, spring, summer, autumn]
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.txt' # save calculation data for analysis
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 + 2), 'end': round_time(base_hour + 5), 'force': force_charge})
2630
- output(f"Charge time: {hours_time(base_hour + 2)}-{hours_time(base_hour + 5)}")
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" {h['date']}: {h['total']:4.1f}"
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, estimated=1 if charge_today else 0, shading=charge_config.get('shading'))
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
- if charge_today:
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
- if charge_today:
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" {d}: {pv_history[d]:4.1f}"
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}%)\n Forecast/Consumption: {expected:.1f}/{consumption:.1f} {expected / consumption * 100:.0f}%")
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
- output(f" Expected SoC at {hours_time(adjusted_hour(time_to_end, time_line))} is {end_soc}%")
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}%)\n Forecast/Consumption: {expected:.1f}/{consumption:.1f} {expected / consumption * 100:.0f}%")
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 {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 ""))
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
- # rebuild the battery residual with the charge added and min_soc
2990
- (bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
2991
- end_residual = interpolate(time_to_end, bat_timed) # residual when charge time ends
2992
- # show the state
2993
- output(f" Start SoC {start_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_start, time_line))} ({start_residual:.2f}kWh)")
2994
- output(f" End SoC {end_residual / capacity * 100:3.0f}% at {hours_time(adjusted_hour(time_to_end, time_line))} ({end_residual:.2f}kWh)")
2995
- # show what we have worked out
2996
- if show_data == 3:
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 %:\n"
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 += " " * (13 if show_data == 2 else 12) * (h % data_wrap)
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 = {current_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 %:\n"
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 += " " * (13 if show_data == 2 else 12) * (h % data_wrap)
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 self.data.get('estimated_actuals') is None else ['forecasts', 'estimated_actuals']:
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())[1:-1]
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 > 2 * days :
3777
- self.keys = self.keys[1:-1]
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 and self.shading.get('solcast') 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['solcast'].get('adjust') is not None:
3789
- loss = self.shading['solcast']['adjust']
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['solcast'].get('am_delay') is not None:
3793
- shaded = time_hours(self.daily[date]['sun'][0]) + self.shading['solcast']['am_delay']
3794
- loss = self.shading['solcast']['am_loss']
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['solcast'].get('pm_delay') is not None:
3798
- shaded = time_hours(self.daily[date]['sun'][1]) - self.shading['solcast']['pm_delay']
3799
- loss = self.shading['solcast']['pm_loss']
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['solar'].get('adjust') is not None:
4105
- loss = self.shading['solar']['adjust']
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['solar'].get('am_delay') is not None:
4109
- shaded = time_hours(self.daily[date]['sun'][0]) + self.shading['solar']['am_delay']
4110
- loss = self.shading['solar']['am_loss']
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['solar'].get('pm_delay') is not None:
4114
- shaded = time_hours(self.daily[date]['sun'][1]) - self.shading['solar']['pm_delay']
4115
- loss = self.shading['solar']['pm_loss']
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}")