foxesscloud 2.7.5__py3-none-any.whl → 2.7.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud
4
- Updated: 13 December 2024
4
+ Updated: 11 January 2025
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.8.6"
13
+ version = "1.8.8"
14
14
  print(f"FoxESS-Cloud version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -435,7 +435,7 @@ device_sn = None
435
435
  var_list = None
436
436
  raw_vars = var_list
437
437
 
438
- def get_device(sn=None):
438
+ def get_device(sn=None, device_type=None):
439
439
  global device_list, device, device_id, device_sn, firmware, battery, var_list, debug_setting, messages, flag, schedule, templates, remote_settings
440
440
  if get_token() is None:
441
441
  return None
@@ -490,31 +490,32 @@ def get_device(sn=None):
490
490
  firmware = get_firmware()
491
491
  remote_settings = get_ui()
492
492
  # parse the model code to work out attributes
493
- model_code = device['deviceType'].upper()
494
- if model_code[:1] == 'T':
495
- model_code = 'T3-' + model_code[1:]
493
+ model_code = device['deviceType'].upper() if device_type is None else device_type
494
+ if model_code[0] in 'FGRST':
495
+ phase = '1' if model_code[0] in 'FGS' else '3'
496
+ model_code = model_code[0] + phase + '-' + model_code[1:]
496
497
  elif model_code[:2] == 'KH':
497
498
  model_code = 'KH-' + model_code[2:]
498
499
  elif model_code[:4] == 'AIO-':
499
500
  model_code = 'AIO' + model_code[4:]
500
- device['eps'] = 'E' in model_code
501
+ device['eps'] = 'E' in model_code[2:]
501
502
  parts = model_code.split('-')
502
503
  model = parts[0]
503
- if model not in ['T3', 'KH', 'H1', 'AC1', 'H3', 'AC3', 'AIOH1', 'AIOH3']:
504
+ if model not in ['F1', 'G1', 'R3', 'S1', 'T3', 'KH', 'H1', 'AC1', 'H3', 'AC3', 'AIOH1', 'AIOH3']:
504
505
  output(f"** device model not recognised for deviceType: {device['deviceType']}")
505
506
  return device
506
507
  device['model'] = model
507
508
  device['phase'] = 3 if model[-1:] == '3' else 1
508
509
  for p in parts[1:]:
509
510
  if p.replace('.','').isnumeric():
510
- power = float(p)
511
- if power >= 1.0 and power < 50.0:
512
- device['power'] = float(p)
511
+ power = float(p) / (1000 if model in ['F1', 'S1'] else 1.0)
512
+ if power >= 0.5 and power < 100.0:
513
+ device['power'] = power
513
514
  break
514
515
  if device.get('power') is None:
515
516
  output(f"** device power not found for deviceType: {device['deviceType']}")
516
517
  # set max charge current
517
- if model in ['T3']:
518
+ if model in ['F1', 'G1', 'R3', 'S1', 'T3']:
518
519
  device['max_charge_current'] = None
519
520
  elif model in ['KH']:
520
521
  device['max_charge_current'] = 50
@@ -2379,7 +2380,7 @@ custom_periods = {'name': 'Custom',
2379
2380
  }
2380
2381
 
2381
2382
  tariff_list = [octopus_flux, intelligent_octopus, octopus_cosy, octopus_go, agile_octopus, bg_driver, eon_drive, economy_7, custom_periods]
2382
- tariff = octopus_flux
2383
+ tariff = None
2383
2384
 
2384
2385
  ##################################################################################################
2385
2386
  # Strategy - schedule templates
@@ -3064,7 +3065,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
3064
3065
  output(f"full_charge = {full_charge}")
3065
3066
  if test_soc is not None:
3066
3067
  current_soc = test_soc
3067
- capacity = 14.40
3068
+ capacity = 14.36
3068
3069
  residual = test_soc * capacity / 100
3069
3070
  bat_volt = 317.4
3070
3071
  bat_power = 0.0
@@ -3856,18 +3857,19 @@ integrate_load_power = 0
3856
3857
  ##################################################################################################
3857
3858
 
3858
3859
  # get pvoutput data for upload to pvoutput api or via Bulk Loader
3859
- # tou: 0 = no time of use, 1 = use time of use periods if available
3860
+ # tou: 0 = no time of use, 1 = use time of use periods if available, 2 = integrate all values
3860
3861
 
3861
3862
  def get_pvoutput(d = None, tou = 0):
3862
3863
  global tariff, pv_calibration, ct2_calibration, integrate_load_power
3863
3864
  if d is None:
3864
3865
  d = date_list()[0]
3865
- tou = 0 if tariff is None else 1 if tou == 1 or tou == True else 0
3866
3866
  if type(d) is list:
3867
3867
  print(f"---------------- get_pvoutput ------------------")
3868
3868
  print(f"Date range {d[0]} to {d[-1]} has {len(d)} days")
3869
3869
  if tou == 1:
3870
3870
  print(f"Time of use: {tariff['name']}")
3871
+ elif tou == 2:
3872
+ print(f"All values integrated from power")
3871
3873
  if integrate_load_power == 1:
3872
3874
  print(f"Consumption integrated from Load Power")
3873
3875
  print(f"------------------------------------------------")
@@ -3881,12 +3883,15 @@ def get_pvoutput(d = None, tou = 0):
3881
3883
  v = ['feedin', 'gridConsumption']
3882
3884
  if integrate_load_power == 0:
3883
3885
  v.append('loads')
3884
- report_data = get_report('day', d=d, v=v, summary=2)
3885
- if report_data is None:
3886
- return None
3886
+ if tou == 2:
3887
+ report_data = []
3888
+ else:
3889
+ report_data = get_report('day', d=d, v=v, summary=2)
3890
+ if report_data is None:
3891
+ return None
3887
3892
  # get raw power data for the day
3888
- v = ['pvPower', 'meterPower2', 'feedinPower', 'gridConsumptionPower'] if tou == 1 else ['pvPower', 'meterPower2']
3889
- if integrate_load_power == 1:
3893
+ v = ['pvPower', 'meterPower2', 'feedinPower', 'gridConsumptionPower'] if tou > 0 else ['pvPower', 'meterPower2']
3894
+ if integrate_load_power == 1 or tou == 2:
3890
3895
  v.append('loadsPower')
3891
3896
  raw_data = get_raw('day', d=d + ' 00:00:00', v=v , summary=1)
3892
3897
  if raw_data is None or len(raw_data) == 0 or raw_data[0].get('kwh') is None or raw_data[0].get('max') is None:
@@ -3916,7 +3921,7 @@ def get_pvoutput(d = None, tou = 0):
3916
3921
  export_tou = ',,,'
3917
3922
  # process list of report_data values (no TOU)
3918
3923
  for var in report_data:
3919
- wh = int(var['total'] * 1000)
3924
+ wh = int(var['total'] * 1000) if var['total'] is not None else 0
3920
3925
  if var['variable'] == 'feedin':
3921
3926
  export_wh = wh
3922
3927
  export = f"{wh},"
@@ -3936,10 +3941,12 @@ def get_pvoutput(d = None, tou = 0):
3936
3941
  generate = f"{wh},"
3937
3942
  power = f"{int(var['max'] * 1000)},{var['max_time']},"
3938
3943
  elif var['variable'] == 'feedinPower':
3944
+ export_wh = wh if tou == 2 else export_wh
3939
3945
  calibrate = export_wh / wh if wh > 0.0 else 1.0
3940
3946
  export = f","
3941
3947
  export_tou = f"{int(peak * calibrate)},{int(off_peak * calibrate)},{int((wh - peak - off_peak) * calibrate)},0"
3942
3948
  elif var['variable'] == 'gridConsumptionPower':
3949
+ grid_wh = wh if tou == 2 else grid_wh
3943
3950
  calibrate = grid_wh / wh if wh > 0.0 else 1.0
3944
3951
  grid = f"{int(peak * calibrate)},{int(off_peak * calibrate)},{int((wh - peak - off_peak) * calibrate)},0,"
3945
3952
  elif var['variable'] == 'loadsPower':
foxesscloud/openapi.py CHANGED
@@ -1,7 +1,7 @@
1
1
  ##################################################################################################
2
2
  """
3
3
  Module: Fox ESS Cloud using Open API
4
- Updated: 13 December 2024
4
+ Updated: 11 January 2025
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.7.5"
13
+ version = "2.7.7"
14
14
  print(f"FoxESS-Cloud Open API version {version}")
15
15
 
16
16
  debug_setting = 1
@@ -419,7 +419,7 @@ device_list = None
419
419
  device = None
420
420
  device_sn = None
421
421
 
422
- def get_device(sn=None):
422
+ def get_device(sn=None, device_type=None):
423
423
  global device_list, device, device_sn, battery, debug_setting, schedule, remote_settings
424
424
  if get_vars() is None:
425
425
  return None
@@ -480,31 +480,32 @@ def get_device(sn=None):
480
480
  get_generation()
481
481
  # remote_settings = get_ui()
482
482
  # parse the model code to work out attributes
483
- model_code = device['deviceType'].upper()
484
- if model_code[:1] == 'T':
485
- model_code = 'T3-' + model_code[1:]
486
- if model_code[:2] == 'KH':
483
+ model_code = device['deviceType'].upper() if device_type is None else device_type
484
+ if model_code[0] in 'FGRST':
485
+ phase = '1' if model_code[0] in 'FGS' else '3'
486
+ model_code = model_code[0] + phase + '-' + model_code[1:]
487
+ elif model_code[:2] == 'KH':
487
488
  model_code = 'KH-' + model_code[2:]
488
489
  elif model_code[:4] == 'AIO-':
489
490
  model_code = 'AIO' + model_code[4:]
490
- device['eps'] = 'E' in model_code
491
+ device['eps'] = 'E' in model_code[2:]
491
492
  parts = model_code.split('-')
492
493
  model = parts[0]
493
- if model not in ['T3', 'KH', 'H1', 'AC1', 'H3', 'AC3', 'AIOH1', 'AIOH3']:
494
+ if model not in ['F1', 'G1', 'R3', 'S1', 'T3', 'KH', 'H1', 'AC1', 'H3', 'AC3', 'AIOH1', 'AIOH3']:
494
495
  output(f"** device model not recognised for deviceType: {device['deviceType']}")
495
496
  return device
496
497
  device['model'] = model
497
498
  device['phase'] = 3 if model[-1:] == '3' else 1
498
499
  for p in parts[1:]:
499
500
  if p.replace('.','').isnumeric():
500
- power = float(p)
501
- if power >= 1.0 and power < 50.0:
502
- device['power'] = float(p)
501
+ power = float(p) / (1000 if model in ['F1', 'S1'] else 1.0)
502
+ if power >= 0.5 and power < 100.0:
503
+ device['power'] = power
503
504
  break
504
505
  if device.get('power') is None:
505
506
  output(f"** device power not found for deviceType: {device['deviceType']}")
506
507
  # set max charge current
507
- if model in ['T3']:
508
+ if model in ['F1', 'G1', 'R3', 'S1', 'T3']:
508
509
  device['max_charge_current'] = None
509
510
  elif model in ['KH']:
510
511
  device['max_charge_current'] = 50
@@ -842,7 +843,7 @@ def get_settings():
842
843
  named_settings = {}
843
844
 
844
845
  def get_remote_settings(name):
845
- global device_sn, debug_setting, messages, name_data
846
+ global device_sn, debug_setting, messages, name_data, named_settings
846
847
  if get_device() is None:
847
848
  return None
848
849
  output(f"getting remote settings", 2)
@@ -878,7 +879,7 @@ def get_named_settings(name):
878
879
  return get_remote_settings(name)
879
880
 
880
881
  def set_named_settings(name, value, force=0):
881
- global device_sn, debug_setting
882
+ global device_sn, debug_setting, named_settings
882
883
  if get_device() is None:
883
884
  return None
884
885
  if force == 1 and get_schedule().get('enable'):
@@ -888,6 +889,10 @@ def set_named_settings(name, value, force=0):
888
889
  for (n, v) in name:
889
890
  result.append(set_named_settings(name=n, value=v))
890
891
  return result
892
+ if named_settings.get(name) is None:
893
+ result = get_named_settings(name)
894
+ if result is None:
895
+ return None
891
896
  output(f"\nSetting {name} to {value}", 1)
892
897
  body = {'sn': device_sn, 'key': name, 'value': f"{value}"}
893
898
  setting_delay()
@@ -901,8 +906,9 @@ def set_named_settings(name, value, force=0):
901
906
  output(f"** cannot update {name} when schedule is active")
902
907
  else:
903
908
  output(f"** set_named_settings(): ({name}, {value}) {errno_message(response)}")
904
- return 0
905
- return 1
909
+ return None
910
+ named_settings[name]['value'] = f"{value}"
911
+ return value
906
912
 
907
913
  ##################################################################################################
908
914
  # wrappers for named settings
@@ -2046,7 +2052,7 @@ custom_periods = {'name': 'Custom',
2046
2052
  }
2047
2053
 
2048
2054
  tariff_list = [octopus_flux, intelligent_octopus, octopus_cosy, octopus_go, agile_octopus, bg_driver, eon_drive, economy_7, custom_periods]
2049
- tariff = octopus_flux
2055
+ tariff = None
2050
2056
 
2051
2057
  ##################################################################################################
2052
2058
  # Strategy - schedule templates
@@ -2730,7 +2736,7 @@ def charge_needed(forecast=None, update_settings=0, timed_mode=None, show_data=N
2730
2736
  output(f"full_charge = {full_charge}")
2731
2737
  if test_soc is not None:
2732
2738
  current_soc = test_soc
2733
- capacity = 14.40
2739
+ capacity = 14.36
2734
2740
  residual = test_soc * capacity / 100
2735
2741
  bat_volt = 317.4
2736
2742
  bat_power = 0.0
@@ -3523,18 +3529,19 @@ integrate_load_power = 0
3523
3529
  ##################################################################################################
3524
3530
 
3525
3531
  # get pvoutput data for upload to pvoutput api or via Bulk Loader
3526
- # tou: 0 = no time of use, 1 = use time of use periods if available
3532
+ # tou: 0 = no time of use, 1 = use time of use periods if available, 2 = integrate all values
3527
3533
 
3528
3534
  def get_pvoutput(d = None, tou = 0):
3529
3535
  global tariff, pv_calibration, ct2_calibration, integrate_load_power
3530
3536
  if d is None:
3531
3537
  d = date_list()[0]
3532
- tou = 0 if tariff is None else 1 if tou == 1 or tou == True else 0
3533
3538
  if type(d) is list:
3534
3539
  print(f"---------------- get_pvoutput ------------------")
3535
3540
  print(f"Date range {d[0]} to {d[-1]} has {len(d)} days")
3536
3541
  if tou == 1:
3537
3542
  print(f"Time of use: {tariff['name']}")
3543
+ elif tou == 2:
3544
+ print(f"All values integrated from power")
3538
3545
  if integrate_load_power == 1:
3539
3546
  print(f"Consumption integrated from Load Power")
3540
3547
  print(f"------------------------------------------------")
@@ -3548,12 +3555,15 @@ def get_pvoutput(d = None, tou = 0):
3548
3555
  v = ['feedin', 'gridConsumption']
3549
3556
  if integrate_load_power == 0:
3550
3557
  v.append('loads')
3551
- report_data = get_report('day', d=d, v=v, summary=2)
3552
- if report_data is None:
3553
- return None
3558
+ if tou == 2:
3559
+ report_data = []
3560
+ else:
3561
+ report_data = get_report('day', d=d, v=v, summary=2)
3562
+ if report_data is None:
3563
+ return None
3554
3564
  # get raw power data for the day
3555
- v = ['pvPower', 'meterPower2', 'feedinPower', 'gridConsumptionPower'] if tou == 1 else ['pvPower', 'meterPower2']
3556
- if integrate_load_power == 1:
3565
+ v = ['pvPower', 'meterPower2', 'feedinPower', 'gridConsumptionPower'] if tou > 0 else ['pvPower', 'meterPower2']
3566
+ if integrate_load_power == 1 or tou == 2:
3557
3567
  v.append('loadsPower')
3558
3568
  raw_data = get_raw('day', d=d + ' 00:00:00', v=v , summary=1)
3559
3569
  if raw_data is None or len(raw_data) == 0 or raw_data[0].get('kwh') is None or raw_data[0].get('max') is None:
@@ -3583,7 +3593,7 @@ def get_pvoutput(d = None, tou = 0):
3583
3593
  export_tou = ',,,'
3584
3594
  # process list of report_data values (no TOU)
3585
3595
  for var in report_data:
3586
- wh = int(var['total'] * 1000)
3596
+ wh = int(var['total'] * 1000) if var['total'] is not None else 0
3587
3597
  if var['variable'] == 'feedin':
3588
3598
  export_wh = wh
3589
3599
  export = f"{wh},"
@@ -3603,10 +3613,12 @@ def get_pvoutput(d = None, tou = 0):
3603
3613
  generate = f"{wh},"
3604
3614
  power = f"{int(var['max'] * 1000)},{var['max_time']},"
3605
3615
  elif var['variable'] == 'feedinPower':
3616
+ export_wh = wh if tou == 2 else export_wh
3606
3617
  calibrate = export_wh / wh if wh > 0.0 else 1.0
3607
3618
  export = f","
3608
3619
  export_tou = f"{int(peak * calibrate)},{int(off_peak * calibrate)},{int((wh - peak - off_peak) * calibrate)},0"
3609
3620
  elif var['variable'] == 'gridConsumptionPower':
3621
+ grid_wh = wh if tou == 2 else grid_wh
3610
3622
  calibrate = grid_wh / wh if wh > 0.0 else 1.0
3611
3623
  grid = f"{int(peak * calibrate)},{int(off_peak * calibrate)},{int((wh - peak - off_peak) * calibrate)},0,"
3612
3624
  elif var['variable'] == 'loadsPower':
@@ -1,4 +1,4 @@
1
- Copyright (c) 2023 Tony Matthews
1
+ Copyright (c) 2023-2025 Tony Matthews
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: foxesscloud
3
- Version: 2.7.5
3
+ Version: 2.7.7
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
@@ -629,7 +629,7 @@ f.get_pvoutput(d, tou)
629
629
  ```
630
630
 
631
631
  + d is the date or a list of dates, to get data for. The default is yesterday.
632
- + tou: optional, setting tou=1 uploads data with time of use. The default, tou=0 does not split data and is more accurate.
632
+ + tou: the default, tou=0, does not split data and is more accurate. Setting tou=1 uploads data with time of use. Setting tou=2 integrates all values and allows set_pvoutput() to work with pv inverters that do not provide energy stats.
633
633
  + setting integrate_load_power to 1 will calculate load energy by integrating the load power instead of using data from Fox. This tries to overcome the limitation where the inverter does not track load power / energy correctly when there is secondary generation. When set to 0 (default), the Fox load energy is used.
634
634
 
635
635
  You can copy and paste the output data to the pvoutput data CSV Loader, using the following settings:
@@ -652,7 +652,7 @@ f.set_pvoutput(d, system_id, tou, push, run_after)
652
652
 
653
653
  + d is optional and is the date, or a list of dates, to upload
654
654
  + system_id is optional and allow you to select where data is uploaded to (where you have more than 1 registered system)
655
- + tou: optional, setting tou=1 uploads data with time of use. The default, tou=0 does not split data and is more accurate
655
+ + tou: the default, tou=0, does not split data and is more accurate. Setting tou=1 uploads data with time of use. Setting tou=2 integrates all values and allows set_pvoutput() to work with pv inverters that do not provide energy stats.
656
656
  + push: optional. 0 = do not sent to pushover, 1 = send summary to pushover, 2 = send first day summary only
657
657
  + run_after: optional. Only generate data on or after this hour. Default 0.
658
658
 
@@ -807,6 +807,15 @@ This setting can be:
807
807
 
808
808
  # Version Info
809
809
 
810
+ 2.7.7<br>
811
+ Updates to get_pvoutput() to support solar inverters that don't provide grid energy by setting tou=2.
812
+ Default tariff changed to None.
813
+
814
+ 2.7.6<br>
815
+ Updates to support F, G, R and S series inverters.
816
+ Updates to set_named_settings() to load metadata if not already done and save new value.
817
+ Fix divide by zero error when using pvoutput with solar only inverters.
818
+
810
819
  2.7.5<br>
811
820
  Update to support T series inverters.
812
821
 
@@ -0,0 +1,7 @@
1
+ foxesscloud/foxesscloud.py,sha256=mh_Yopg3c5Hx8rl1LVet58EUnzacoUjjh0g2n68KwPQ,223311
2
+ foxesscloud/openapi.py,sha256=e9Uf9NTAFPWTh2xzCyTg-6vS8obogmwok-41u5c1lhE,207725
3
+ foxesscloud-2.7.7.dist-info/LICENCE,sha256=8JF-24QkE8UfdII-g6RaIEvM-PZ9zwaEcxlwYUDMt-4,1079
4
+ foxesscloud-2.7.7.dist-info/METADATA,sha256=luevOoQZLqPj4c74WkyG6FLYFmlq6GwNd_O3xJ3J490,63042
5
+ foxesscloud-2.7.7.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
+ foxesscloud-2.7.7.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
+ foxesscloud-2.7.7.dist-info/RECORD,,
@@ -1,7 +0,0 @@
1
- foxesscloud/foxesscloud.py,sha256=b7DcJs3Ms538CPyMvl8llTile5-gYR6bi31Jf7linqQ,222841
2
- foxesscloud/openapi.py,sha256=wn3dJhTth0IKNOT8Uv4bMYxiB5W1WVTEVJhNQqu7pMw,207033
3
- foxesscloud-2.7.5.dist-info/LICENCE,sha256=-3xv8CElCJV8Bc8PbAsg3iyxMpAK8MoJneM3rXigxqI,1074
4
- foxesscloud-2.7.5.dist-info/METADATA,sha256=NaUsXWSxDtnQFURfkVYYjTQ4eIqYsze3CKg1rvgIpno,62438
5
- foxesscloud-2.7.5.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
6
- foxesscloud-2.7.5.dist-info/top_level.txt,sha256=IWOrKSNZCLU6IDXSX_b4_bqCfbZoWAT4CC0w0Lg7PuU,12
7
- foxesscloud-2.7.5.dist-info/RECORD,,