foxesscloud 2.5.8__tar.gz → 2.5.9__tar.gz

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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.5.8
3
+ Version: 2.5.9
4
4
  Summary: library for accessing Fox ESS cloud data using Open API
5
5
  Author-email: Tony Matthews <tony@quasair.co.uk>
6
6
  Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
@@ -363,44 +363,46 @@ Given the data available, the modelling works as follows:
363
363
 
364
364
  The following parameters and default values are used to configure charge_needed and may be updated if required using name=value:
365
365
  ```
366
- contingency: [20,10,5,10] # % of consumption. Single or [winter, spring, summer, autumn] values
367
- capacity: None # Battery capacity in kWh (over-rides generated value if set)
368
- charge_current: None # max battery charge current setting in A. None uses a value derrived from the inverter model
369
- discharge_current: None # max battery discharge current setting in A. None uses a value derrived from the inverter model
370
- export_limit: None # maximum export power in kW. None uses the inverter power rating
371
- discharge_loss: 0.98 # loss converting battery discharge power to grid power
372
- pv_loss: 0.95 # loss converting PV power to battery charge power
373
- grid_loss: 0.975 # loss converting grid power to battery charge power
374
- inverter_power: None # inverter power consumption in W (dynamically set)
375
- bms_power: 50 # BMS power consumption in W
376
- force_charge_power: 5.00 # power used when Force Charge is scheduled
377
- allowed_drain: 4, # % tolerance below min_soc before float charge starts
378
- float_current: 4, # BMS float charge current in A
379
- bat_resistance: 0.070 # internal resistance of a battery in ohms
380
- volt_curve: lifepo4_curve # battery OCV from 0% to 100% SoC
381
- nominal_soc: 55 # SoC for nominal open circuit voltage
382
- generation_days: 3 # number of days to use for average generation (1-7)
383
- consumption_days: 3 # number of days to use for average consumption (1-7)
384
- consumption_span: 'week' # 'week' = last 7 days or 'weekday' = last 7 weekdays e.g. Saturdays
385
- use_today: 21.0 # hour when today's generation and consumption data will be used
386
- min_hours: 0.25 # minimum charge time to set (in decimal hours)
387
- min_kwh: 0.5 # minimum charge to add in kwh
388
- solcast_adjust: 100 # % adjustment to make to Solcast forecast
389
- solar_adjust: 100 # % adjustment to make to Solar forecast
390
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
391
- annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
392
- timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
393
- special_contingency: 30 # contingency for special days when consumption might be higher
366
+ contingency: [20,10,5,10] # % of consumption. Single or [winter, spring, summer, autumn] values
367
+ capacity: None # Battery capacity in kWh (over-rides generated value if set)
368
+ charge_current: None # max battery charge current setting in A. None uses a value derrived from the inverter model
369
+ discharge_current: None # max battery discharge current setting in A. None uses a value derrived from the inverter model
370
+ export_limit: None # maximum export power in kW. None uses the inverter power rating
371
+ dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
372
+ pv_loss: 0.950 # loss converting PV power to DC battery charge power
373
+ ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
374
+ charge_loss: [0.975, 1.040] # loss in battery energy for each kWh added (based on residual_handling)
375
+ discharge_loss: [0.975, 0.975] # loss in battery energy for each kWh removed (based on residual_handling)
376
+ inverter_power: None # inverter power consumption in W (dynamically set)
377
+ bms_power: 50 # BMS power consumption in W
378
+ force_charge_power: 5.00 # power used when Force Charge is scheduled
379
+ allowed_drain: 4, # % tolerance below min_soc before float charge starts
380
+ float_current: 4, # BMS float charge current in A
381
+ bat_resistance: 0.070 # internal resistance of a battery in ohms
382
+ volt_curve: lifepo4_curve # battery OCV from 0% to 100% SoC
383
+ nominal_soc: 55 # SoC for nominal open circuit voltage
384
+ generation_days: 3 # number of days to use for average generation (1-7)
385
+ consumption_days: 3 # number of days to use for average consumption (1-7)
386
+ consumption_span: 'week' # 'week' = last 7 days or 'weekday' = last 7 weekdays e.g. Saturdays
387
+ use_today: 21.0 # hour when today's generation and consumption data will be used
388
+ min_hours: 0.25 # minimum charge time to set (in decimal hours)
389
+ min_kwh: 0.5 # minimum charge to add in kwh
390
+ solcast_adjust: 100 # % adjustment to make to Solcast forecast
391
+ solar_adjust: 100 # % adjustment to make to Solar forecast
392
+ forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
393
+ annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
394
+ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
395
+ special_contingency: 30 # contingency for special days when consumption might be higher
394
396
  special_days: ['12-25', '12-26', '01-01']
395
- full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
396
- derate_temp: 21 # battery temperature in C when derating charge current is applied
397
- derate_step: 5 # step size for derating e.g. 21, 16, 11
398
- derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 21C, 16C, 11C, 6C
399
- force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
400
- data_wrap: 6 # data items to show per line
401
- target_soc: None # target soc for charging
402
- shading: {} # effect of shading on Solcast / Solar (see below)
403
- save: 'charge_needed.txt' # where to save calculation data for charge_compare(). '###' gets replaced with todays date.
397
+ full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
398
+ derate_temp: 28 # battery temperature in C when derating charge current is applied
399
+ derate_step: 5 # step size for derating e.g. 21, 16, 11
400
+ derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 28C, 23C, 18C, 13C
401
+ force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
402
+ data_wrap: 6 # data items to show per line
403
+ target_soc: None # target soc for charging (over-rides calculated value)
404
+ shading: {} # effect of shading on Solcast / Solar (see below)
405
+ save: 'charge_needed.txt' # where to save calculation data for charge_compare(). '###' gets replaced with todays date.
404
406
  ```
405
407
 
406
408
  These values are stored / available in f.charge_config.
@@ -784,6 +786,11 @@ This setting can be:
784
786
 
785
787
  # Version Info
786
788
 
789
+ 2.5.9<br>
790
+ Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
791
+ Update charge calibration for new BMS firmware.
792
+ Increase de-rating temperature from 21C to 28C for new BMS firmware.
793
+
787
794
  2.5.8<br>
788
795
  Fix incorrect charging setup when force_charge=1.
789
796
  Rework charge_periods() to consolidate charge periods to reduce number of time segments when timed_mode=2.
@@ -349,44 +349,46 @@ Given the data available, the modelling works as follows:
349
349
 
350
350
  The following parameters and default values are used to configure charge_needed and may be updated if required using name=value:
351
351
  ```
352
- contingency: [20,10,5,10] # % of consumption. Single or [winter, spring, summer, autumn] values
353
- capacity: None # Battery capacity in kWh (over-rides generated value if set)
354
- charge_current: None # max battery charge current setting in A. None uses a value derrived from the inverter model
355
- discharge_current: None # max battery discharge current setting in A. None uses a value derrived from the inverter model
356
- export_limit: None # maximum export power in kW. None uses the inverter power rating
357
- discharge_loss: 0.98 # loss converting battery discharge power to grid power
358
- pv_loss: 0.95 # loss converting PV power to battery charge power
359
- grid_loss: 0.975 # loss converting grid power to battery charge power
360
- inverter_power: None # inverter power consumption in W (dynamically set)
361
- bms_power: 50 # BMS power consumption in W
362
- force_charge_power: 5.00 # power used when Force Charge is scheduled
363
- allowed_drain: 4, # % tolerance below min_soc before float charge starts
364
- float_current: 4, # BMS float charge current in A
365
- bat_resistance: 0.070 # internal resistance of a battery in ohms
366
- volt_curve: lifepo4_curve # battery OCV from 0% to 100% SoC
367
- nominal_soc: 55 # SoC for nominal open circuit voltage
368
- generation_days: 3 # number of days to use for average generation (1-7)
369
- consumption_days: 3 # number of days to use for average consumption (1-7)
370
- consumption_span: 'week' # 'week' = last 7 days or 'weekday' = last 7 weekdays e.g. Saturdays
371
- use_today: 21.0 # hour when today's generation and consumption data will be used
372
- min_hours: 0.25 # minimum charge time to set (in decimal hours)
373
- min_kwh: 0.5 # minimum charge to add in kwh
374
- solcast_adjust: 100 # % adjustment to make to Solcast forecast
375
- solar_adjust: 100 # % adjustment to make to Solar forecast
376
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
377
- annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
378
- timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
379
- special_contingency: 30 # contingency for special days when consumption might be higher
352
+ contingency: [20,10,5,10] # % of consumption. Single or [winter, spring, summer, autumn] values
353
+ capacity: None # Battery capacity in kWh (over-rides generated value if set)
354
+ charge_current: None # max battery charge current setting in A. None uses a value derrived from the inverter model
355
+ discharge_current: None # max battery discharge current setting in A. None uses a value derrived from the inverter model
356
+ export_limit: None # maximum export power in kW. None uses the inverter power rating
357
+ dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
358
+ pv_loss: 0.950 # loss converting PV power to DC battery charge power
359
+ ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
360
+ charge_loss: [0.975, 1.040] # loss in battery energy for each kWh added (based on residual_handling)
361
+ discharge_loss: [0.975, 0.975] # loss in battery energy for each kWh removed (based on residual_handling)
362
+ inverter_power: None # inverter power consumption in W (dynamically set)
363
+ bms_power: 50 # BMS power consumption in W
364
+ force_charge_power: 5.00 # power used when Force Charge is scheduled
365
+ allowed_drain: 4, # % tolerance below min_soc before float charge starts
366
+ float_current: 4, # BMS float charge current in A
367
+ bat_resistance: 0.070 # internal resistance of a battery in ohms
368
+ volt_curve: lifepo4_curve # battery OCV from 0% to 100% SoC
369
+ nominal_soc: 55 # SoC for nominal open circuit voltage
370
+ generation_days: 3 # number of days to use for average generation (1-7)
371
+ consumption_days: 3 # number of days to use for average consumption (1-7)
372
+ consumption_span: 'week' # 'week' = last 7 days or 'weekday' = last 7 weekdays e.g. Saturdays
373
+ use_today: 21.0 # hour when today's generation and consumption data will be used
374
+ min_hours: 0.25 # minimum charge time to set (in decimal hours)
375
+ min_kwh: 0.5 # minimum charge to add in kwh
376
+ solcast_adjust: 100 # % adjustment to make to Solcast forecast
377
+ solar_adjust: 100 # % adjustment to make to Solar forecast
378
+ forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
379
+ annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
380
+ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
381
+ special_contingency: 30 # contingency for special days when consumption might be higher
380
382
  special_days: ['12-25', '12-26', '01-01']
381
- full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
382
- derate_temp: 21 # battery temperature in C when derating charge current is applied
383
- derate_step: 5 # step size for derating e.g. 21, 16, 11
384
- derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 21C, 16C, 11C, 6C
385
- force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
386
- data_wrap: 6 # data items to show per line
387
- target_soc: None # target soc for charging
388
- shading: {} # effect of shading on Solcast / Solar (see below)
389
- save: 'charge_needed.txt' # where to save calculation data for charge_compare(). '###' gets replaced with todays date.
383
+ full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
384
+ derate_temp: 28 # battery temperature in C when derating charge current is applied
385
+ derate_step: 5 # step size for derating e.g. 21, 16, 11
386
+ derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 28C, 23C, 18C, 13C
387
+ force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
388
+ data_wrap: 6 # data items to show per line
389
+ target_soc: None # target soc for charging (over-rides calculated value)
390
+ shading: {} # effect of shading on Solcast / Solar (see below)
391
+ save: 'charge_needed.txt' # where to save calculation data for charge_compare(). '###' gets replaced with todays date.
390
392
  ```
391
393
 
392
394
  These values are stored / available in f.charge_config.
@@ -770,6 +772,11 @@ This setting can be:
770
772
 
771
773
  # Version Info
772
774
 
775
+ 2.5.9<br>
776
+ Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
777
+ Update charge calibration for new BMS firmware.
778
+ Increase de-rating temperature from 21C to 28C for new BMS firmware.
779
+
773
780
  2.5.8<br>
774
781
  Fix incorrect charging setup when force_charge=1.
775
782
  Rework charge_periods() to consolidate charge periods to reduce number of time segments when timed_mode=2.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "foxesscloud"
7
- version = "2.5.8"
7
+ version = "2.5.9"
8
8
  authors = [
9
9
  {name="Tony Matthews", email="tony@quasair.co.uk"},
10
10
  ]
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud
4
- Updated: 01 October 2024
4
+ Updated: 02 October 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.7.0"
13
+ version = "1.7.1"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -576,7 +576,9 @@ def get_firmware():
576
576
 
577
577
  battery = None
578
578
  battery_settings = None
579
- residual_handling = 1 # set to 2 if Residual returns current capacity
579
+
580
+ # 1 = returns Residual Energy. 2 = resturns Residual Capacity
581
+ residual_handling = 1
580
582
 
581
583
  def get_battery(info=0):
582
584
  global token, device_id, battery, debug_setting, messages
@@ -2592,23 +2594,23 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
2592
2594
  return work_mode_timed
2593
2595
 
2594
2596
  # build the timed battery residual from the charge / discharge, work mode and min_soc
2597
+ # all power values are as measured at the inverter battery connection
2595
2598
  def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=None, reserve_drain=None):
2596
- global charge_config, steps_per_hour
2597
- bat_timed = []
2599
+ global charge_config, steps_per_hour, residual_handling
2598
2600
  allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
2599
2601
  bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
2600
- charge_loss = charge_config['charge_loss']
2602
+ charge_loss = charge_config['charge_loss'][residual_handling - 1]
2603
+ discharge_loss = charge_config['discharge_loss'][residual_handling - 1]
2601
2604
  charge_limit = charge_config['charge_limit']
2602
2605
  float_charge = charge_config['float_charge']
2603
2606
  for i in range(0, len(work_mode_timed)):
2604
- bat_timed.append(kwh_current)
2605
2607
  w = work_mode_timed[i]
2606
2608
  w['kwh'] = kwh_current
2607
2609
  max_now = w['max_soc'] * capacity / 100
2608
2610
  if kwh_current < max_now and w['charge'] > 0.0:
2609
2611
  kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
2610
2612
  kwh_current = max_now if kwh_current > max_now else kwh_current
2611
- kwh_current += (w['pv'] - w['discharge']) / charge_loss / steps_per_hour
2613
+ kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
2612
2614
  if kwh_current > capacity:
2613
2615
  # battery is full
2614
2616
  kwh_current = capacity
@@ -2629,16 +2631,16 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2629
2631
  reserve_drain = reserve_now
2630
2632
  if kwh_min is not None and kwh_current < kwh_min and i >= time_to_next: # track minimum without charge
2631
2633
  kwh_min = kwh_current
2632
- return (bat_timed, kwh_min)
2634
+ return ([work_mode_timed[i]['kwh'] for i in range(0, len(work_mode_timed))], kwh_min)
2633
2635
 
2636
+ # use work_mode_timed to generate time periods for the inverter schedule
2634
2637
  def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
2635
2638
  global steps_per_hour
2636
- output(f"\nConfiguring schedule:",1)
2637
2639
  strategy = []
2638
2640
  start = base_hour
2639
- periods = []
2641
+ times = []
2640
2642
  for t in range(0, min([24 * steps_per_hour, len(work_mode_timed)])):
2641
- period = periods[0] if len(periods) > 0 else work_mode_timed[0]
2643
+ period = times[0] if len(times) > 0 else work_mode_timed[0]
2642
2644
  next_period = work_mode_timed[t]
2643
2645
  h = base_hour + t / steps_per_hour
2644
2646
  if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold']:
@@ -2650,15 +2652,19 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
2650
2652
  s['max_soc'] = period.get('max_soc')
2651
2653
  elif period['mode'] == 'SelfUse' and period['hold'] == 1:
2652
2654
  s['min_soc'] = min([int(period['kwh'] / capacity * 100 + 0.5), 100])
2653
- for p in periods:
2655
+ s['end'] = (start + 1 / steps_per_hour) % 24
2656
+ for p in times:
2654
2657
  p['min_soc'] = s['min_soc']
2655
2658
  if s['mode'] != 'SelfUse' or s['min_soc'] != min_soc:
2656
2659
  strategy.append(s)
2657
2660
  start = h
2658
- periods = []
2659
- periods.append(work_mode_timed[t])
2660
- if len(strategy) > 0 and strategy[-1]['min_soc'] != min_soc:
2661
+ times = []
2662
+ times.append(work_mode_timed[t])
2663
+ if len(strategy) == 0:
2664
+ return []
2665
+ if strategy[-1]['min_soc'] != min_soc:
2661
2666
  strategy.append({'start': start %24, 'end': (start + 1 / steps_per_hour) % 24, 'mode': 'SelfUse', 'min_soc': min_soc})
2667
+ output(f"\nConfiguring schedule:",1)
2662
2668
  periods = []
2663
2669
  for s in strategy:
2664
2670
  periods.append(set_period(segment = s, quiet=0))
@@ -2682,9 +2688,11 @@ charge_config = {
2682
2688
  'charge_current': None, # max battery charge current setting in A
2683
2689
  'discharge_current': None, # max battery discharge current setting in A
2684
2690
  'export_limit': None, # maximum export power in kW
2685
- 'discharge_loss': 0.97, # loss converting battery discharge power to grid power
2686
- 'pv_loss': 0.95, # loss converting PV power to battery charge power
2687
- 'grid_loss': 0.975, # loss converting grid power to battery charge power
2691
+ 'dc_ac_loss': 0.970, # loss converting battery DC power to AC grid power
2692
+ 'pv_loss': 0.950, # loss converting PV power to DC battery charge power
2693
+ 'ac_dc_loss': 0.960, # loss converting AC grid power to DC battery charge power
2694
+ 'charge_loss': [0.975, 1.040], # loss in battery energy for each kWh added (based on residual_handling)
2695
+ 'discharge_loss': [0.975, 0.975], # loss in battery energy for each kWh removed (based on residual_handling)
2688
2696
  'inverter_power': 101, # Inverter power consumption in W
2689
2697
  'bms_power': 50, # BMS power consumption in W
2690
2698
  'force_charge_power': 5.00, # charge power in kW when using force charge
@@ -2705,11 +2713,11 @@ charge_config = {
2705
2713
  'special_contingency': 33, # contingency for special days when consumption might be higher
2706
2714
  'special_days': ['12-25', '12-26', '01-01'],
2707
2715
  'full_charge': None, # day of month (1-28) to do full charge, or 'daily' or 'Mon', 'Tue' etc
2708
- 'derate_temp': 22, # battery temperature where cold derating starts to be applied
2716
+ 'derate_temp': 28, # BMS temperature when cold derating starts to be applied
2709
2717
  'derate_step': 5, # scale for derating factors in C
2710
- 'derating': [24, 15, 10, 2], # max charge current e.g. 5C step = 22C, 17C, 12C, 7C
2718
+ 'derating': [24, 15, 10, 2], # max charge current de-rating
2711
2719
  'data_wrap': 6, # data items to show per line
2712
- 'target_soc': None, # set the target SoC for charging
2720
+ 'target_soc': None, # the target SoC for charging (over-rides calculated value)
2713
2721
  'shading': { # effect of shading on Solcast / forecast.solar
2714
2722
  'solcast': {'adjust': 0.95, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2},
2715
2723
  'solar': {'adjust': 1.20, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2}
@@ -2732,7 +2740,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
2732
2740
  def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
2733
2741
  forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
2734
2742
  global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
2735
- global timed_strategy, steps_per_hour, base_time, storage
2743
+ global timed_strategy, steps_per_hour, base_time, storage, residual_handling
2736
2744
  print(f"\n---------------- charge_needed ----------------")
2737
2745
  # validate parameters
2738
2746
  args = locals()
@@ -2858,7 +2866,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2858
2866
  current_soc = test_soc
2859
2867
  capacity = 14.54
2860
2868
  residual = test_soc * capacity / 100
2861
- bat_volt = 315.4
2869
+ bat_volt = 317.4
2862
2870
  bat_power = 0.0
2863
2871
  temperature = 30
2864
2872
  bat_current = 0.0
@@ -2909,35 +2917,35 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2909
2917
  bms_power = charge_config['bms_power']
2910
2918
  bms_loss = bms_power / 1000
2911
2919
  # work out charge limit, power and losses. Max power going to the battery after ac conversion losses
2920
+ ac_dc_loss = charge_config['ac_dc_loss']
2912
2921
  charge_limit = min([charge_current * (bat_ocv + charge_current * bat_resistance) / 1000, max([6, device_power])])
2913
2922
  if charge_limit < 0.1:
2914
2923
  output(f"** charge_current is too low ({charge_current:.1f}A)")
2915
- charge_loss = 1.0 - charge_limit * 1000 * bat_resistance / bat_ocv ** 2
2916
2924
  force_charge_power = charge_config['force_charge_power'] if timed_mode > 1 and charge_config.get('force_charge_power') is not None else 100
2917
- grid_loss = charge_config['grid_loss']
2918
- charge_power = min([(device_power - operating_loss) * grid_loss, force_charge_power * grid_loss, charge_limit])
2925
+ charge_power = min([(device_power - operating_loss) * ac_dc_loss, force_charge_power * ac_dc_loss, charge_limit])
2919
2926
  float_charge = (charge_config['float_current'] if charge_config.get('float_current') is not None else 4) * bat_ocv / 1000
2920
- charge_config['charge_loss'] = charge_loss
2921
2927
  charge_config['charge_limit'] = charge_limit
2922
2928
  charge_config['charge_power'] = charge_power
2923
2929
  charge_config['float_charge'] = float_charge
2930
+ charge_loss = charge_config['charge_loss'][residual_handling - 1]
2924
2931
  # work out discharge limit = max power coming from the battery before ac conversion losses
2925
- discharge_loss = charge_config['discharge_loss']
2926
- discharge_limit = device_power / discharge_loss
2932
+ dc_ac_loss = charge_config['dc_ac_loss']
2933
+ discharge_limit = device_power / dc_ac_loss
2927
2934
  discharge_current = device_current if charge_config['discharge_current'] is None else charge_config['discharge_current']
2928
2935
  discharge_power = discharge_current * bat_ocv / 1000
2929
2936
  discharge_limit = discharge_power if discharge_power < discharge_limit else discharge_limit
2937
+ discharge_loss = charge_config['discharge_loss'][residual_handling - 1]
2930
2938
  # charging happens if generation exceeds export limit in feedin work mode
2931
2939
  export_power = device_power if charge_config['export_limit'] is None else charge_config['export_limit']
2932
- export_limit = export_power / discharge_loss
2940
+ export_limit = export_power / dc_ac_loss
2933
2941
  current_mode = get_work_mode()
2934
2942
  output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
2935
2943
  output(f"\nDevice Info:")
2936
2944
  output(f" Model: {model}")
2937
2945
  output(f" Rating: {device_power:.2f}kW")
2938
2946
  output(f" Export: {export_power:.2f}kW")
2939
- output(f" Charge: {charge_current:.1f}A, {charge_power:.2f}kW, {charge_loss * 100:.1f}% efficient")
2940
- output(f" Discharge: {discharge_current:.1f}A, {discharge_limit:.2f}kW, {discharge_loss * 100:.1f}% efficient")
2947
+ output(f" Charge: {charge_current:.1f}A, {charge_power:.2f}kW, {ac_dc_loss * 100:.1f}% efficient")
2948
+ output(f" Discharge: {discharge_current:.1f}A, {discharge_limit:.2f}kW, {dc_ac_loss * 100:.1f}% efficient")
2941
2949
  output(f" Inverter: {inverter_power:.0f}W power consumption")
2942
2950
  output(f" BMS: {bms_power:.0f}W power consumption")
2943
2951
  if current_mode is not None:
@@ -3041,7 +3049,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3041
3049
  update_settings = 0
3042
3050
  # produce time lines for charge, discharge and work mode
3043
3051
  charge_timed = [min([charge_limit, x * charge_config['pv_loss']]) for x in generation_timed]
3044
- discharge_timed = [min([discharge_limit, x / discharge_loss]) + bms_loss for x in consumption_timed]
3052
+ discharge_timed = [min([discharge_limit, x / dc_ac_loss]) + bms_loss for x in consumption_timed]
3045
3053
  work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
3046
3054
  for i in range(0, len(work_mode_timed)):
3047
3055
  # get work mode
@@ -3052,9 +3060,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3052
3060
  discharge_timed[i] = discharge_timed[i] * (1.0 - duration)
3053
3061
  work_mode_timed[i]['charge'] = charge_power * duration
3054
3062
  elif timed_mode > 0 and work_mode == 'ForceDischarge':
3055
- fdpwr = work_mode_timed[i]['fdpwr'] / discharge_loss / 1000
3056
- fdpwr = min([discharge_limit, export_limit + discharge_timed[i] * charge_loss, fdpwr])
3057
- discharge_timed[i] = fdpwr * duration / charge_loss + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
3063
+ fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
3064
+ fdpwr = min([discharge_limit, export_limit + discharge_timed[i], fdpwr])
3065
+ discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
3058
3066
  elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
3059
3067
  discharge_timed[i] = bms_loss
3060
3068
  if timed_mode > 1:
@@ -3099,13 +3107,13 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3099
3107
  end_timed = time_to_end
3100
3108
  end_soc = int(end_residual / capacity * 100 + 0.5)
3101
3109
  else:
3102
- if test_charge is None:
3103
- output(f"\nCharge needed: {kwh_needed:.2f}kWh")
3104
- charge_message = "with charge added"
3105
- output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
3106
3110
  # work out time to add kwh_needed to battery
3107
3111
  charge_rate = charge_power * charge_loss
3108
3112
  hours = kwh_needed / charge_rate
3113
+ if test_charge is None:
3114
+ output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
3115
+ charge_message = "with charge added"
3116
+ output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
3109
3117
  # check if charge time exceeded or charge needed exceeds capacity
3110
3118
  hours_to_full = (capacity - start_residual) / charge_rate
3111
3119
  if hours > charge_time:
@@ -3146,7 +3154,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3146
3154
  if i >= start_timed and i < end_timed:
3147
3155
  work_mode_timed[i]['mode'] = 'ForceCharge'
3148
3156
  work_mode_timed[i]['charge'] = charge_power * t
3149
- work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else 100
3157
+ work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
3150
3158
  work_mode_timed[i]['discharge'] *= (1-t)
3151
3159
  # rebuild the battery residual with the charge added and min_soc
3152
3160
  (bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
@@ -3218,7 +3226,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3218
3226
  data['capacity'] = capacity
3219
3227
  data['config'] = charge_config
3220
3228
  data['time'] = time_line
3221
- data['bat'] = bat_timed
3222
3229
  data['work_mode'] = work_mode_timed
3223
3230
  data['generation'] = generation_timed
3224
3231
  data['consumption'] = consumption_timed
@@ -3253,10 +3260,10 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
3253
3260
  steps_per_hour = data.get('steps')
3254
3261
  capacity = data.get('capacity')
3255
3262
  time_line = data.get('time')
3256
- bat_timed = data.get('bat')
3257
3263
  generation_timed = data.get('generation')
3258
3264
  consumption_timed = data.get('consumption')
3259
3265
  work_mode_timed = data.get('work_mode')
3266
+ bat_timed = data['bat'] if data.get('bat') is not None else [work_mode_timed[t]['kwh'] for t in range(0, len(work_mode_timed))]
3260
3267
  run_time = len(time_line)
3261
3268
  base_hour = int(time_hours(base_time[11:16]))
3262
3269
  start_day = base_time[:10]
@@ -3298,7 +3305,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
3298
3305
  s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
3299
3306
  h = base_hour
3300
3307
  t = 0
3301
- while t < len(time_line) and bat_timed[t] is not None:
3308
+ while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
3302
3309
  col = h % data_wrap
3303
3310
  s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
3304
3311
  s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 01 October 2024
4
+ Updated: 02 October 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.8"
13
+ version = "2.5.9"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -541,7 +541,9 @@ battery = None
541
541
  battery_settings = None
542
542
  battery_vars = ['SoC', 'invBatVolt', 'invBatCurrent', 'invBatPower', 'batTemperature', 'ResidualEnergy' ]
543
543
  battery_data = ['soc', 'volt', 'current', 'power', 'temperature', 'residual']
544
- residual_handling = 1 # set to 2 if Residual returns current capacity
544
+
545
+ # 1 = returns Residual Energy. 2 = resturns Residual Capacity
546
+ residual_handling = 1
545
547
 
546
548
  def get_battery(v = None, info=0):
547
549
  global device_sn, battery, debug_setting, residual_handling
@@ -2455,23 +2457,23 @@ def strategy_timed(timed_mode, base_hour, run_time, min_soc=10, max_soc=100, cur
2455
2457
  return work_mode_timed
2456
2458
 
2457
2459
  # build the timed battery residual from the charge / discharge, work mode and min_soc
2460
+ # note: all power values are as measured at the inverter battery connection
2458
2461
  def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=None, reserve_drain=None):
2459
- global charge_config, steps_per_hour
2460
- bat_timed = []
2462
+ global charge_config, steps_per_hour, residual_handling
2461
2463
  allowed_drain = charge_config['allowed_drain'] if charge_config.get('allowed_drain') is not None else 4
2462
2464
  bms_loss = (charge_config['bms_power'] / 1000 if charge_config.get('bms_power') is not None else 0.05)
2463
- charge_loss = charge_config['charge_loss']
2465
+ charge_loss = charge_config['charge_loss'][residual_handling - 1]
2466
+ discharge_loss = charge_config['discharge_loss'][residual_handling - 1]
2464
2467
  charge_limit = charge_config['charge_limit']
2465
2468
  float_charge = charge_config['float_charge']
2466
2469
  for i in range(0, len(work_mode_timed)):
2467
- bat_timed.append(kwh_current)
2468
2470
  w = work_mode_timed[i]
2469
2471
  w['kwh'] = kwh_current
2470
2472
  max_now = w['max_soc'] * capacity / 100
2471
2473
  if kwh_current < max_now and w['charge'] > 0.0:
2472
2474
  kwh_current += min([w['charge'], charge_limit - w['pv']]) * charge_loss / steps_per_hour
2473
2475
  kwh_current = max_now if kwh_current > max_now else kwh_current
2474
- kwh_current += (w['pv'] - w['discharge']) / charge_loss / steps_per_hour
2476
+ kwh_current += (w['pv'] * charge_loss - w['discharge'] / discharge_loss) / steps_per_hour
2475
2477
  if kwh_current > capacity:
2476
2478
  # battery is full
2477
2479
  kwh_current = capacity
@@ -2492,16 +2494,16 @@ def battery_timed(work_mode_timed, kwh_current, capacity, time_to_next, kwh_min=
2492
2494
  reserve_drain = reserve_now
2493
2495
  if kwh_min is not None and kwh_current < kwh_min and i >= time_to_next: # track minimum without charge
2494
2496
  kwh_min = kwh_current
2495
- return (bat_timed, kwh_min)
2497
+ return ([work_mode_timed[i]['kwh'] for i in range(0, len(work_mode_timed))], kwh_min)
2496
2498
 
2499
+ # use work_mode_timed to generate time periods for the inverter schedule
2497
2500
  def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
2498
2501
  global steps_per_hour
2499
- output(f"\nConfiguring schedule:",1)
2500
2502
  strategy = []
2501
2503
  start = base_hour
2502
- periods = []
2504
+ times = []
2503
2505
  for t in range(0, min([24 * steps_per_hour, len(work_mode_timed)])):
2504
- period = periods[0] if len(periods) > 0 else work_mode_timed[0]
2506
+ period = times[0] if len(times) > 0 else work_mode_timed[0]
2505
2507
  next_period = work_mode_timed[t]
2506
2508
  h = base_hour + t / steps_per_hour
2507
2509
  if h == 24 or period['mode'] != next_period['mode'] or period['hold'] != next_period['hold']:
@@ -2513,15 +2515,19 @@ def charge_periods(work_mode_timed, base_hour, min_soc, capacity):
2513
2515
  s['max_soc'] = period.get('max_soc')
2514
2516
  elif period['mode'] == 'SelfUse' and period['hold'] == 1:
2515
2517
  s['min_soc'] = min([int(period['kwh'] / capacity * 100 + 0.5), 100])
2516
- for p in periods:
2518
+ s['end'] = (start + 1 / steps_per_hour) % 24
2519
+ for p in times:
2517
2520
  p['min_soc'] = s['min_soc']
2518
2521
  if s['mode'] != 'SelfUse' or s['min_soc'] != min_soc:
2519
2522
  strategy.append(s)
2520
2523
  start = h
2521
- periods = []
2522
- periods.append(work_mode_timed[t])
2523
- if len(strategy) > 0 and strategy[-1]['min_soc'] != min_soc:
2524
+ times = []
2525
+ times.append(work_mode_timed[t])
2526
+ if len(strategy) == 0:
2527
+ return []
2528
+ if strategy[-1]['min_soc'] != min_soc:
2524
2529
  strategy.append({'start': start %24, 'end': (start + 1 / steps_per_hour) % 24, 'mode': 'SelfUse', 'min_soc': min_soc})
2530
+ output(f"\nConfiguring schedule:",1)
2525
2531
  periods = []
2526
2532
  for s in strategy:
2527
2533
  periods.append(set_period(segment = s, quiet=0))
@@ -2545,9 +2551,11 @@ charge_config = {
2545
2551
  'charge_current': None, # max battery charge current setting in A
2546
2552
  'discharge_current': None, # max battery discharge current setting in A
2547
2553
  'export_limit': None, # maximum export power in kW
2548
- 'discharge_loss': 0.97, # loss converting battery discharge power to grid power
2549
- 'pv_loss': 0.95, # loss converting PV power to battery charge power
2550
- 'grid_loss': 0.975, # loss converting grid power to battery charge power
2554
+ 'dc_ac_loss': 0.97, # loss converting battery DC power to AC grid power
2555
+ 'pv_loss': 0.95, # loss converting PV power to DC battery charge power
2556
+ 'ac_dc_loss': 0.962, # loss converting AC grid power to DC battery charge power
2557
+ 'charge_loss': [0.975, 1.040], # loss in battery energy for each kWh added based on residual_handling
2558
+ 'discharge_loss': [0.975, 0.975], # loss in battery energy for each kWh removed based on residual_handling
2551
2559
  'inverter_power': 101, # Inverter power consumption in W
2552
2560
  'bms_power': 50, # BMS power consumption in W
2553
2561
  'force_charge_power': 5.00, # charge power in kW when using force charge
@@ -2568,11 +2576,11 @@ charge_config = {
2568
2576
  'special_contingency': 33, # contingency for special days when consumption might be higher
2569
2577
  'special_days': ['12-25', '12-26', '01-01'],
2570
2578
  'full_charge': None, # day of month (1-28) to do full charge, or 'daily' or 'Mon', 'Tue' etc
2571
- 'derate_temp': 22, # battery temperature where cold derating starts to be applied
2579
+ 'derate_temp': 25, # BMS temperature when cold derating starts to be applied
2572
2580
  'derate_step': 5, # scale for derating factors in C
2573
- 'derating': [24, 15, 10, 2], # max charge current e.g. 5C step = 22C, 17C, 12C, 7C
2581
+ 'derating': [24, 15, 10, 2], # max charge current e.g. 5C step = 25C, 20C, 15C, 10C
2574
2582
  'data_wrap': 6, # data items to show per line
2575
- 'target_soc': None, # set the target SoC for charging
2583
+ 'target_soc': None, # the target SoC for charging (over-rides calculated value)
2576
2584
  'shading': { # effect of shading on Solcast / forecast.solar
2577
2585
  'solcast': {'adjust': 0.95, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2},
2578
2586
  'solar': {'adjust': 1.20, 'am_delay': 1.0, 'am_loss': 0.2, 'pm_delay': 1.0, 'pm_loss': 0.2}
@@ -2595,7 +2603,7 @@ charge_needed_app_key = "awcr5gro2v13oher3v1qu6hwnovp28"
2595
2603
  def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=None, show_plot=None, run_after=None, reload=2,
2596
2604
  forecast_times=None, force_charge=0, test_time=None, test_soc=None, test_charge=None, **settings):
2597
2605
  global device, seasonality, solcast_api_key, debug_setting, tariff, solar_arrays, legend_location, time_shift, charge_needed_app_key
2598
- global timed_strategy, steps_per_hour, base_time, storage
2606
+ global timed_strategy, steps_per_hour, base_time, storage, residual_handling
2599
2607
  print(f"\n---------------- charge_needed ----------------")
2600
2608
  # validate parameters
2601
2609
  args = locals()
@@ -2721,7 +2729,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2721
2729
  current_soc = test_soc
2722
2730
  capacity = 14.54
2723
2731
  residual = test_soc * capacity / 100
2724
- bat_volt = 315.4
2732
+ bat_volt = 317.4
2725
2733
  bat_power = 0.0
2726
2734
  temperature = 30
2727
2735
  bat_current = 0.0
@@ -2772,35 +2780,35 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2772
2780
  bms_power = charge_config['bms_power']
2773
2781
  bms_loss = bms_power / 1000
2774
2782
  # work out charge limit, power and losses. Max power going to the battery after ac conversion losses
2783
+ ac_dc_loss = charge_config['ac_dc_loss']
2775
2784
  charge_limit = min([charge_current * (bat_ocv + charge_current * bat_resistance) / 1000, max([6, device_power])])
2776
2785
  if charge_limit < 0.1:
2777
2786
  output(f"** charge_current is too low ({charge_current:.1f}A)")
2778
- charge_loss = 1.0 - charge_limit * 1000 * bat_resistance / bat_ocv ** 2
2779
2787
  force_charge_power = charge_config['force_charge_power'] if timed_mode > 1 and charge_config.get('force_charge_power') is not None else 100
2780
- grid_loss = charge_config['grid_loss']
2781
- charge_power = min([(device_power - operating_loss) * grid_loss, force_charge_power * grid_loss, charge_limit])
2788
+ charge_power = min([(device_power - operating_loss) * ac_dc_loss, force_charge_power * ac_dc_loss, charge_limit])
2782
2789
  float_charge = (charge_config['float_current'] if charge_config.get('float_current') is not None else 4) * bat_ocv / 1000
2783
- charge_config['charge_loss'] = charge_loss
2784
2790
  charge_config['charge_limit'] = charge_limit
2785
2791
  charge_config['charge_power'] = charge_power
2786
2792
  charge_config['float_charge'] = float_charge
2793
+ charge_loss = charge_config['charge_loss'][residual_handling - 1]
2787
2794
  # work out discharge limit = max power coming from the battery before ac conversion losses
2788
- discharge_loss = charge_config['discharge_loss']
2789
- discharge_limit = device_power / discharge_loss
2795
+ dc_ac_loss = charge_config['dc_ac_loss']
2796
+ discharge_limit = device_power / dc_ac_loss
2790
2797
  discharge_current = device_current if charge_config['discharge_current'] is None else charge_config['discharge_current']
2791
2798
  discharge_power = discharge_current * bat_ocv / 1000
2792
2799
  discharge_limit = discharge_power if discharge_power < discharge_limit else discharge_limit
2800
+ discharge_loss = charge_config['discharge_loss'][residual_handling - 1]
2793
2801
  # charging happens if generation exceeds export limit in feedin work mode
2794
2802
  export_power = device_power if charge_config['export_limit'] is None else charge_config['export_limit']
2795
- export_limit = export_power / discharge_loss
2803
+ export_limit = export_power / dc_ac_loss
2796
2804
  current_mode = get_work_mode()
2797
2805
  output(f"\ncharge_config = {json.dumps(charge_config, indent=2)}", 3)
2798
2806
  output(f"\nDevice Info:")
2799
2807
  output(f" Model: {model}")
2800
2808
  output(f" Rating: {device_power:.2f}kW")
2801
2809
  output(f" Export: {export_power:.2f}kW")
2802
- output(f" Charge: {charge_current:.1f}A, {charge_power:.2f}kW, {charge_loss * 100:.1f}% efficient")
2803
- output(f" Discharge: {discharge_current:.1f}A, {discharge_limit:.2f}kW, {discharge_loss * 100:.1f}% efficient")
2810
+ output(f" Charge: {charge_current:.1f}A, {charge_power:.2f}kW, {ac_dc_loss * 100:.1f}% efficient")
2811
+ output(f" Discharge: {discharge_current:.1f}A, {discharge_limit:.2f}kW, {dc_ac_loss * 100:.1f}% efficient")
2804
2812
  output(f" Inverter: {inverter_power:.0f}W power consumption")
2805
2813
  output(f" BMS: {bms_power:.0f}W power consumption")
2806
2814
  if current_mode is not None:
@@ -2903,7 +2911,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2903
2911
  update_settings = 0
2904
2912
  # produce time lines for charge, discharge and work mode
2905
2913
  charge_timed = [min([charge_limit, x * charge_config['pv_loss']]) for x in generation_timed]
2906
- discharge_timed = [min([discharge_limit, x / discharge_loss]) + bms_loss for x in consumption_timed]
2914
+ discharge_timed = [min([discharge_limit, x / dc_ac_loss]) + bms_loss for x in consumption_timed]
2907
2915
  work_mode_timed = strategy_timed(timed_mode, base_hour, run_time, min_soc=min_soc, max_soc=max_soc, current_mode=current_mode)
2908
2916
  for i in range(0, len(work_mode_timed)):
2909
2917
  # get work mode
@@ -2914,9 +2922,9 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2914
2922
  discharge_timed[i] = discharge_timed[i] * (1.0 - duration)
2915
2923
  work_mode_timed[i]['charge'] = charge_power * duration
2916
2924
  elif timed_mode > 0 and work_mode == 'ForceDischarge':
2917
- fdpwr = work_mode_timed[i]['fdpwr'] / discharge_loss / 1000
2918
- fdpwr = min([discharge_limit, export_limit + discharge_timed[i] * charge_loss, fdpwr])
2919
- discharge_timed[i] = fdpwr * duration / charge_loss + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
2925
+ fdpwr = work_mode_timed[i]['fdpwr'] / dc_ac_loss / 1000
2926
+ fdpwr = min([discharge_limit, export_limit + discharge_timed[i], fdpwr])
2927
+ discharge_timed[i] = fdpwr * duration + discharge_timed[i] * (1.0 - duration) - charge_timed[i] * duration
2920
2928
  elif bat_hold > 0 and i >= int(time_to_start) and i < int(time_to_end):
2921
2929
  discharge_timed[i] = bms_loss
2922
2930
  if timed_mode > 1:
@@ -2961,13 +2969,13 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2961
2969
  end_timed = time_to_end
2962
2970
  end_soc = int(end_residual / capacity * 100 + 0.5)
2963
2971
  else:
2964
- if test_charge is None:
2965
- output(f"\nCharge needed: {kwh_needed:.2f}kWh:")
2966
- charge_message = "with charge added"
2967
- output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
2968
2972
  # work out time to add kwh_needed to battery
2969
2973
  charge_rate = charge_power * charge_loss
2970
2974
  hours = kwh_needed / charge_rate
2975
+ if test_charge is None:
2976
+ output(f"\nCharge needed: {kwh_needed:.2f}kWh ({hours_time(hours)})")
2977
+ charge_message = "with charge added"
2978
+ output(f" SoC now: {current_soc:.0f}% at {hours_time(hour_now)} on {today}")
2971
2979
  # check if charge time exceeded or charge needed exceeds capacity
2972
2980
  hours_to_full = (capacity - start_residual) / charge_rate
2973
2981
  if hours > charge_time:
@@ -3008,7 +3016,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3008
3016
  if i >= start_timed and i < end_timed:
3009
3017
  work_mode_timed[i]['mode'] = 'ForceCharge'
3010
3018
  work_mode_timed[i]['charge'] = charge_power * t
3011
- work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else 100
3019
+ work_mode_timed[i]['max_soc'] = target_soc if target_soc is not None else max_soc
3012
3020
  work_mode_timed[i]['discharge'] *= (1-t)
3013
3021
  # rebuild the battery residual with any charge added and min_soc
3014
3022
  (bat_timed, x) = battery_timed(work_mode_timed, kwh_current, capacity, time_to_next)
@@ -3080,7 +3088,6 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3080
3088
  data['capacity'] = capacity
3081
3089
  data['config'] = charge_config
3082
3090
  data['time'] = time_line
3083
- data['bat'] = bat_timed
3084
3091
  data['work_mode'] = work_mode_timed
3085
3092
  data['generation'] = generation_timed
3086
3093
  data['consumption'] = consumption_timed
@@ -3114,10 +3121,10 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
3114
3121
  steps_per_hour = data.get('steps')
3115
3122
  capacity = data.get('capacity')
3116
3123
  time_line = data.get('time')
3117
- bat_timed = data.get('bat')
3118
3124
  generation_timed = data.get('generation')
3119
3125
  consumption_timed = data.get('consumption')
3120
3126
  work_mode_timed = data.get('work_mode')
3127
+ bat_timed = data['bat'] if data.get('bat') is not None else [work_mode_timed[t]['kwh'] for t in range(0, len(work_mode_timed))]
3121
3128
  run_time = len(time_line)
3122
3129
  base_hour = int(time_hours(base_time[11:16]))
3123
3130
  start_day = base_time[:10]
@@ -3159,7 +3166,7 @@ def charge_compare(save=None, v=None, show_data=1, show_plot=3):
3159
3166
  s = f"\nBattery Energy kWh:" if show_data == 2 else f"\nBattery SoC:"
3160
3167
  h = base_hour
3161
3168
  t = 0
3162
- while t < len(time_line) and bat_timed[t] is not None:
3169
+ while t < len(time_line) and bat_timed[t] is not None and plots['SoC'][t] is not None:
3163
3170
  col = h % data_wrap
3164
3171
  s += f"\n {hours_time(time_line[t])}" if t == 0 or col == 0 else ""
3165
3172
  s += f" {plots['SoC'][t]:5.2f}" if show_data == 2 else f" {plots['SoC'][t] / capacity * 100:3.0f}%"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.5.8
3
+ Version: 2.5.9
4
4
  Summary: library for accessing Fox ESS cloud data using Open API
5
5
  Author-email: Tony Matthews <tony@quasair.co.uk>
6
6
  Project-URL: Homepage, https://github.com/TonyM1958/FoxESS-Cloud
@@ -363,44 +363,46 @@ Given the data available, the modelling works as follows:
363
363
 
364
364
  The following parameters and default values are used to configure charge_needed and may be updated if required using name=value:
365
365
  ```
366
- contingency: [20,10,5,10] # % of consumption. Single or [winter, spring, summer, autumn] values
367
- capacity: None # Battery capacity in kWh (over-rides generated value if set)
368
- charge_current: None # max battery charge current setting in A. None uses a value derrived from the inverter model
369
- discharge_current: None # max battery discharge current setting in A. None uses a value derrived from the inverter model
370
- export_limit: None # maximum export power in kW. None uses the inverter power rating
371
- discharge_loss: 0.98 # loss converting battery discharge power to grid power
372
- pv_loss: 0.95 # loss converting PV power to battery charge power
373
- grid_loss: 0.975 # loss converting grid power to battery charge power
374
- inverter_power: None # inverter power consumption in W (dynamically set)
375
- bms_power: 50 # BMS power consumption in W
376
- force_charge_power: 5.00 # power used when Force Charge is scheduled
377
- allowed_drain: 4, # % tolerance below min_soc before float charge starts
378
- float_current: 4, # BMS float charge current in A
379
- bat_resistance: 0.070 # internal resistance of a battery in ohms
380
- volt_curve: lifepo4_curve # battery OCV from 0% to 100% SoC
381
- nominal_soc: 55 # SoC for nominal open circuit voltage
382
- generation_days: 3 # number of days to use for average generation (1-7)
383
- consumption_days: 3 # number of days to use for average consumption (1-7)
384
- consumption_span: 'week' # 'week' = last 7 days or 'weekday' = last 7 weekdays e.g. Saturdays
385
- use_today: 21.0 # hour when today's generation and consumption data will be used
386
- min_hours: 0.25 # minimum charge time to set (in decimal hours)
387
- min_kwh: 0.5 # minimum charge to add in kwh
388
- solcast_adjust: 100 # % adjustment to make to Solcast forecast
389
- solar_adjust: 100 # % adjustment to make to Solar forecast
390
- forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
391
- annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
392
- timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
393
- special_contingency: 30 # contingency for special days when consumption might be higher
366
+ contingency: [20,10,5,10] # % of consumption. Single or [winter, spring, summer, autumn] values
367
+ capacity: None # Battery capacity in kWh (over-rides generated value if set)
368
+ charge_current: None # max battery charge current setting in A. None uses a value derrived from the inverter model
369
+ discharge_current: None # max battery discharge current setting in A. None uses a value derrived from the inverter model
370
+ export_limit: None # maximum export power in kW. None uses the inverter power rating
371
+ dc_ac_loss: 0.970 # loss converting battery DC power to AC grid power
372
+ pv_loss: 0.950 # loss converting PV power to DC battery charge power
373
+ ac_dc_loss: 0.960 # loss converting AC grid power to DC battery charge power
374
+ charge_loss: [0.975, 1.040] # loss in battery energy for each kWh added (based on residual_handling)
375
+ discharge_loss: [0.975, 0.975] # loss in battery energy for each kWh removed (based on residual_handling)
376
+ inverter_power: None # inverter power consumption in W (dynamically set)
377
+ bms_power: 50 # BMS power consumption in W
378
+ force_charge_power: 5.00 # power used when Force Charge is scheduled
379
+ allowed_drain: 4, # % tolerance below min_soc before float charge starts
380
+ float_current: 4, # BMS float charge current in A
381
+ bat_resistance: 0.070 # internal resistance of a battery in ohms
382
+ volt_curve: lifepo4_curve # battery OCV from 0% to 100% SoC
383
+ nominal_soc: 55 # SoC for nominal open circuit voltage
384
+ generation_days: 3 # number of days to use for average generation (1-7)
385
+ consumption_days: 3 # number of days to use for average consumption (1-7)
386
+ consumption_span: 'week' # 'week' = last 7 days or 'weekday' = last 7 weekdays e.g. Saturdays
387
+ use_today: 21.0 # hour when today's generation and consumption data will be used
388
+ min_hours: 0.25 # minimum charge time to set (in decimal hours)
389
+ min_kwh: 0.5 # minimum charge to add in kwh
390
+ solcast_adjust: 100 # % adjustment to make to Solcast forecast
391
+ solar_adjust: 100 # % adjustment to make to Solar forecast
392
+ forecast_selection: 1 # 1 = only update charge times if forecast is available, 0 = use best available data. Default is 1.
393
+ annual_consumption: None # optional annual consumption in kWh. If set, this replaces consumption history
394
+ timed_mode: 0 # 0 = None, 1 = use timed work mode, 2 = strategy mode
395
+ special_contingency: 30 # contingency for special days when consumption might be higher
394
396
  special_days: ['12-25', '12-26', '01-01']
395
- full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
396
- derate_temp: 21 # battery temperature in C when derating charge current is applied
397
- derate_step: 5 # step size for derating e.g. 21, 16, 11
398
- derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 21C, 16C, 11C, 6C
399
- force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
400
- data_wrap: 6 # data items to show per line
401
- target_soc: None # target soc for charging
402
- shading: {} # effect of shading on Solcast / Solar (see below)
403
- save: 'charge_needed.txt' # where to save calculation data for charge_compare(). '###' gets replaced with todays date.
397
+ full_charge: None # day of month (1-28) to do full charge or 'daily' or day of week: 'Mon', 'Tue' etc
398
+ derate_temp: 28 # battery temperature in C when derating charge current is applied
399
+ derate_step: 5 # step size for derating e.g. 21, 16, 11
400
+ derating: [24, 15, 10, 2] # derated charge current for each temperature step e.g. 28C, 23C, 18C, 13C
401
+ force: 1 # 1 = disable strategy periods when setting charge. 0 = fail if strategy period has been set.
402
+ data_wrap: 6 # data items to show per line
403
+ target_soc: None # target soc for charging (over-rides calculated value)
404
+ shading: {} # effect of shading on Solcast / Solar (see below)
405
+ save: 'charge_needed.txt' # where to save calculation data for charge_compare(). '###' gets replaced with todays date.
404
406
  ```
405
407
 
406
408
  These values are stored / available in f.charge_config.
@@ -784,6 +786,11 @@ This setting can be:
784
786
 
785
787
  # Version Info
786
788
 
789
+ 2.5.9<br>
790
+ Change loss parameters to separate AC/DC, DC/AC conversion losses and battery charge / discharge losses.
791
+ Update charge calibration for new BMS firmware.
792
+ Increase de-rating temperature from 21C to 28C for new BMS firmware.
793
+
787
794
  2.5.8<br>
788
795
  Fix incorrect charging setup when force_charge=1.
789
796
  Rework charge_periods() to consolidate charge periods to reduce number of time segments when timed_mode=2.
File without changes
File without changes