Skip to content

Commit 02c8971

Browse files
GECloud fixes for Gateway
Smoothing data fix for after the last sample
1 parent 94a9294 commit 02c8971

6 files changed

Lines changed: 420 additions & 91 deletions

File tree

‎apps/predbat/execute.py‎

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -725,31 +725,76 @@ def fetch_inverter_data(self, create=True):
725725
self.reserve_current = dp3(self.reserve_current)
726726
self.reserve_current_percent = calc_percent_limit(self.reserve_current, self.soc_max)
727727

728-
self.log(
729-
"Found {} inverters totals: min reserve {} current reserve {} soc_max {} soc {} charge rate {} kW discharge rate {} kW battery_rate_min {} w ac limit {} export limit {} kW loss charge {} % loss discharge {} % inverter loss {} %".format(
730-
len(self.inverters),
731-
self.reserve,
732-
self.reserve_current,
733-
self.soc_max,
734-
self.soc_kw,
735-
self.charge_rate_now * MINUTE_WATT,
736-
self.discharge_rate_now * MINUTE_WATT,
737-
self.battery_rate_min * MINUTE_WATT,
738-
dp3(self.inverter_limit * MINUTE_WATT),
739-
dp3(self.export_limit * MINUTE_WATT),
740-
100 - int(self.battery_loss * 100),
741-
100 - int(self.battery_loss_discharge * 100),
742-
100 - int(self.inverter_loss * 100),
728+
if self.debug_enable:
729+
self.log(
730+
"Found {} inverters totals: min reserve {} current reserve {} soc_max {} soc {} charge rate {} kW discharge rate {} kW battery_rate_min {} w ac limit {} export limit {} kW loss charge {} % loss discharge {} % inverter loss {} %".format(
731+
len(self.inverters),
732+
self.reserve,
733+
self.reserve_current,
734+
self.soc_max,
735+
self.soc_kw,
736+
self.charge_rate_now * MINUTE_WATT,
737+
self.discharge_rate_now * MINUTE_WATT,
738+
self.battery_rate_min * MINUTE_WATT,
739+
dp3(self.inverter_limit * MINUTE_WATT),
740+
dp3(self.export_limit * MINUTE_WATT),
741+
100 - int(self.battery_loss * 100),
742+
100 - int(self.battery_loss_discharge * 100),
743+
100 - int(self.inverter_loss * 100),
744+
)
743745
)
744-
)
745746

746747
# Work out current charge limits and publish charge limit base
747748
self.charge_limit = [self.current_charge_limit * self.soc_max / 100.0 for i in range(len(self.charge_window))]
748749
self.charge_limit_percent = calc_percent_limit(self.charge_limit, self.soc_max)
749750
self.publish_charge_limit(self.charge_limit, self.charge_window, self.charge_limit_percent, best=False)
751+
self.publish_inverter_data()
752+
753+
def publish_inverter_data(self):
754+
"""
755+
Publish inverter data to dashboard
756+
"""
757+
self.dashboard_item(
758+
self.prefix + ".pv_power",
759+
state=dp3(self.pv_power / 1000.0),
760+
attributes={
761+
"friendly_name": "Predicted PV Power",
762+
"state_class": "measurement",
763+
"unit_of_measurement": "kW",
764+
"icon": "mdi:battery",
765+
},
766+
)
767+
self.dashboard_item(
768+
self.prefix + ".grid_power",
769+
state=dp3(self.grid_power / 1000.0),
770+
attributes={
771+
"friendly_name": "Predicted Grid Power",
772+
"state_class": "measurement",
773+
"unit_of_measurement": "kW",
774+
"icon": "mdi:battery",
775+
},
776+
)
777+
self.dashboard_item(
778+
self.prefix + ".load_power",
779+
state=dp3(self.load_power / 1000.0),
780+
attributes={
781+
"friendly_name": "Predicted Load Power",
782+
"state_class": "measurement",
783+
"unit_of_measurement": "kW",
784+
"icon": "mdi:battery",
785+
},
786+
)
787+
self.dashboard_item(
788+
self.prefix + ".battery_power",
789+
state=dp3(self.battery_power / 1000.0),
790+
attributes={
791+
"friendly_name": "Predicted Battery Power",
792+
"state_class": "measurement",
793+
"unit_of_measurement": "kW",
794+
"icon": "mdi:battery",
795+
},
796+
)
750797

751-
self.log("Base charge window {}".format(self.window_as_text(self.charge_window, self.charge_limit_percent)))
752-
self.log("Base export window {}".format(self.window_as_text(self.export_window, self.export_limits)))
753798

754799
def balance_inverters(self):
755800
"""

‎apps/predbat/fetch.py‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ def minute_data(
532532
prev_last_updated_time=None,
533533
last_state=0,
534534
attributes=False,
535+
max_increment=MAX_INCREMENT,
535536
):
536537
"""
537538
Turns data from HA into a hash of data indexed by minute with the data being the value
@@ -540,8 +541,8 @@ def minute_data(
540541
mdata = {}
541542
adata = {}
542543
newest_state = 0
544+
prev_state = 0
543545
newest_age = 999999
544-
max_increment = MAX_INCREMENT
545546

546547
# Bounds on the data we store
547548
minute_min = -days * 24 * 60
@@ -680,6 +681,7 @@ def minute_data(
680681

681682
if minutes < newest_age:
682683
newest_age = minutes
684+
prev_state = newest_state
683685
newest_state = state
684686

685687
# Power to Energy
@@ -749,6 +751,21 @@ def minute_data(
749751

750752
# If we only have a start time then fill the gaps with the last values
751753
if not to_key:
754+
# Fill from last sample until now with interolation if enabled
755+
if clean_increment and backwards:
756+
last_sample_minute = 0
757+
for minute in range(60 * 24 * days):
758+
if minute in mdata:
759+
last_sample_minute = minute
760+
break
761+
last_sample_value = mdata[last_sample_minute]
762+
last_but_one_minute_sample = mdata[last_sample_minute + 1] if (last_sample_minute + 1) in mdata else last_sample_value
763+
step = last_sample_value - last_but_one_minute_sample
764+
if last_sample_minute < 10*60 and last_sample_minute > 0:
765+
for minute in range(last_sample_minute):
766+
if minute not in mdata:
767+
mdata[minute] = dp4(newest_state + step * (last_sample_minute - minute))
768+
752769
# Fill from last sample until now
753770
for minute in range(60 * 24 * days):
754771
if backwards:

‎apps/predbat/gecloud.py‎

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,10 @@
153153
EVC_BLACKLIST_COMMANDS = ["installation-mode", "perform-factory-reset", "rename-id-tag", "delete-id-tags", "change-randomised-delay-duration"]
154154

155155
TIMEOUT = 240
156-
RETRIES = 5
156+
RETRIES = 10
157+
RETRY_FACTOR = 1
157158
MAX_THREADS = 2
159+
MAX_START_TIME = 10*60
158160

159161
attribute_table = {
160162
"time": {"friendly_name": "Time", "icon": "mdi:clock", "unit_of_measurement": "Time", "state_class": "timestamp"},
@@ -224,7 +226,7 @@ def wait_api_started(self):
224226
"""
225227
self.log("GECloud: Waiting for API to start")
226228
count = 0
227-
while not self.api_started and count < 240:
229+
while not self.api_started and count < MAX_START_TIME:
228230
time.sleep(1)
229231
count += 1
230232
if not self.api_started:
@@ -372,6 +374,7 @@ async def publish_info(self, device, device_info):
372374
attributes = {}
373375
if key == "info":
374376
info = device_info[key]
377+
last_updated = device_info.get("last_updated", None)
375378
cap = info.get("battery", {}).get("nominal_capacity", None)
376379
volt = info.get("battery", {}).get("nominal_voltage", None)
377380
dod = info.get("battery", {}).get("depth_of_discharge", None)
@@ -389,6 +392,7 @@ async def publish_info(self, device, device_info):
389392
self.base.dashboard_item(entity_name + "_battery_size", capacity, attributes=attribute_table.get("battery_size", {}), app="gecloud")
390393
self.base.dashboard_item(entity_name + "_max_charge_rate", max_charge_rate, attributes=attribute_table.get("max_charge_rate", {}), app="gecloud")
391394
self.base.dashboard_item(entity_name + "_battery_dod", dod, attributes=attribute_table.get("_battery_dod", {}), app="gecloud")
395+
self.base.dashboard_item(entity_name + "_last_updated", last_updated, attributes=attribute_table.get("time", {}), app="gecloud")
392396

393397
async def publish_evc_data(self, serial, evc_data):
394398
"""
@@ -642,6 +646,7 @@ async def async_automatic_config(self, devices):
642646
return
643647

644648
batteries = devices["battery"]
649+
batteries_real = devices["battery"]
645650
num_inverters = len(batteries)
646651

647652
if devices["gateway"]:
@@ -670,21 +675,34 @@ async def async_automatic_config(self, devices):
670675
self.base.args["charge_limit"] = ["number.predbat_gecloud_" + device + "_ac_charge_upper_percent_limit" for device in batteries]
671676
self.base.args["discharge_start_time"] = ["select.predbat_gecloud_" + device + "_dc_discharge_1_start_time" for device in batteries]
672677
self.base.args["discharge_end_time"] = ["select.predbat_gecloud_" + device + "_dc_discharge_1_end_time" for device in batteries]
673-
self.base.args["discharge_target_soc"] = ["select.predbat_gecloud_" + device + "_dc_discharge_1_lower_soc_percent_limit" for device in batteries]
678+
self.base.args["discharge_target_soc"] = ["number.predbat_gecloud_" + device + "_dc_discharge_1_lower_soc_percent_limit" for device in batteries]
674679
self.base.args["scheduled_charge_enable"] = ["switch.predbat_gecloud_" + device + "_ac_charge_enable" for device in batteries]
675680
self.base.args["scheduled_discharge_enable"] = ["switch.predbat_gecloud_" + device + "_enable_dc_discharge" for device in batteries]
676681
self.base.args["pause_mode"] = ["select.predbat_gecloud_" + device + "_pause_battery" for device in batteries]
677682
self.base.args["pause_start_time"] = ["select.predbat_gecloud_" + device + "_pause_battery_start_time" for device in batteries]
678683
self.base.args["pause_end_time"] = ["select.predbat_gecloud_" + device + "_pause_battery_end_time" for device in batteries]
684+
685+
# Do we have a charge power percentage setting?
686+
for device in batteries:
687+
registers = self.settings.get(device, {})
688+
for key in registers:
689+
reg_name = registers[key].get("name", None)
690+
value = registers[key].get("value", None)
691+
if "inverter_charge_power_percentage" in reg_name.lower():
692+
self.log("GECloud: Found charge power percentage setting for {}, using this".format(device))
693+
self.base.args["charge_rate_percent"] = ["number.predbat_gecloud_" + device + "_inverter_charge_power_percentage" for device in batteries]
694+
self.base.args["discharge_rate_percent"] = ["number.predbat_gecloud_" + device + "_inverter_discharge_power_percentage" for device in batteries]
695+
break
696+
679697
if "givtcp_rest" in self.base.args:
680698
del self.base.args["givtcp_rest"]
681-
699+
682700
# Use the first battery serial for the ge_cloud_serial (for status)
683701
self.base.args["ge_cloud_serial"] = devices["battery"][0]
684702

685703
# reconfigure for EMS
686704
if devices["ems"]:
687-
self.log("GECloud: EMS detected")
705+
self.log("GECloud: EMS detected, using this for control")
688706
ems = devices["ems"]
689707
self.base.args["inverter_type"] = ["GEE" for _ in range(num_inverters)]
690708
self.base.args["ge_cloud_serial"] = ems
@@ -747,9 +765,6 @@ async def start(self):
747765
for device in device_list:
748766
self.pending_writes[device] = []
749767

750-
if self.automatic:
751-
await self.async_automatic_config(devices_dict)
752-
753768
seconds = 0
754769
while not self.stop_cloud and not self.base.fatal_error:
755770
try:
@@ -774,8 +789,10 @@ async def start(self):
774789
self.settings[device] = await self.async_get_inverter_settings(device, first=False, previous=self.settings.get(device, {}))
775790
await self.publish_registers(device, self.settings[device])
776791

777-
# Real time control disable?
792+
# One shot tasks
778793
if seconds == 0:
794+
if self.automatic:
795+
await self.async_automatic_config(devices_dict)
779796
for device in device_list:
780797
await self.enable_real_time_control(device, self.settings[device])
781798

@@ -813,7 +830,7 @@ async def async_send_evc_command(self, uuid, command, params):
813830
data = None
814831
if data:
815832
break
816-
await asyncio.sleep(1 * (retry + 1))
833+
await asyncio.sleep(RETRY_FACTOR * (retry + 1))
817834
if data is None:
818835
self.log("Error: GECloud: Failed to send EVC command {} params {}".format(command, params))
819836
return data
@@ -835,7 +852,7 @@ async def async_read_inverter_setting(self, serial, setting_id):
835852
if serial in self.pending_writes:
836853
for pending in self.pending_writes[serial]:
837854
if pending["setting_id"] == setting_id:
838-
return {"value": pending["value"]}
855+
return {"value": pending["value"], "context": "predbat"}
839856

840857
for retry in range(RETRIES):
841858
data = await self.async_get_inverter_data(GE_API_INVERTER_READ_SETTING, serial, setting_id, post=True)
@@ -847,12 +864,12 @@ async def async_read_inverter_setting(self, serial, setting_id):
847864
elif data and data_value in [-1, -2, -5, -6]:
848865
data = None
849866
# Inverter timeout, try to spread requests out
850-
await asyncio.sleep(random.random() * 2)
867+
await asyncio.sleep(random.random() * (3 + retry))
851868
if data:
852869
break
853-
await asyncio.sleep(1 * (retry + 1))
870+
await asyncio.sleep(RETRY_FACTOR * (retry + 1))
854871
if data is None:
855-
self.log("Warn: GECloud: Device {} Failed to read inverter setting id {}".format(serial, setting_id))
872+
self.log("Warn: GECloud: Device {} Failed to read inverter setting id {} got {}".format(serial, setting_id, data))
856873
return data
857874

858875
async def async_write_inverter_setting(self, serial, setting_id, value):
@@ -865,15 +882,15 @@ async def async_write_inverter_setting(self, serial, setting_id, value):
865882
serial,
866883
setting_id,
867884
post=True,
868-
datain={"value": str(value), "context": "homeassistant"},
885+
datain={"value": str(value), "context": "predbat"},
869886
)
870887
if data and "success" in data:
871888
if not data["success"]:
872889
data = None
873890
if data:
874891
self.pending_writes[serial].append({"setting_id": setting_id, "value": value})
875892
break
876-
await asyncio.sleep(1 * (retry + 1))
893+
await asyncio.sleep(RETRY_FACTOR * (retry + 1))
877894
if data is None:
878895
self.log("Warn: GECloud: Failed to write setting id {} value {}".format(setting_id, value))
879896
return data
@@ -1169,6 +1186,7 @@ async def async_get_devices(self):
11691186
self.log("GECloud: Found device {}".format(device))
11701187
inverter = device.get("inverter", {})
11711188
serial = inverter.get("serial", None)
1189+
last_updated = inverter.get("last_updated", None)
11721190
model = inverter.get("info", {}).get("model", "").lower()
11731191
batteries = inverter.get("connections", {}).get("batteries", [])
11741192
if serial:
@@ -1202,7 +1220,7 @@ async def async_get_inverter_data_retry(self, endpoint, serial="", setting_id=""
12021220
data = await self.async_get_inverter_data(endpoint, serial, setting_id, post, datain, uuid, meter_ids, start_time=start_time, end_time=end_time, command=command, measurands=measurands)
12031221
if data is not None:
12041222
break
1205-
await asyncio.sleep(1 * (retry + 1))
1223+
await asyncio.sleep(RETRY_FACTOR * (retry + 1))
12061224
if data is None:
12071225
self.log("Warn: GECloud: Failed to get data from {}".format(endpoint))
12081226
return data
@@ -1237,7 +1255,7 @@ async def async_get_inverter_data(self, endpoint, serial="", setting_id="", post
12371255
self.log(f"Warn: GECloud: Exception during request to {url}: {e}")
12381256
self.failures_total += 1
12391257
return None
1240-
1258+
12411259
try:
12421260
data = response.json()
12431261
except requests.exceptions.JSONDecodeError:
@@ -1397,7 +1415,6 @@ def download_ge_data(self, now_utc):
13971415
self.export_today_now = self.export_today.get(0, 0) - self.export_today.get(self.minutes_now, 0)
13981416
self.pv_today_now = self.pv_today.get(0, 0) - self.pv_today.get(self.minutes_now, 0)
13991417

1400-
self.log("GE GECloud load_last_period is {} kW".format(dp2(self.load_last_period)))
14011418
# More up to date sensors for current values if set
14021419
if "load_today" in self.args:
14031420
load_minutes, load_minutes_age = self.minute_data_load(self.now_utc, "load_today", self.max_days_previous, required_unit="kWh", load_scaling=self.load_scaling)

‎apps/predbat/inverter.py‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -389,7 +389,7 @@ def __init__(self, base, id=0, quiet=False, rest_postCommand=None, rest_getData=
389389
tdiff = dp2(tdiff.seconds / 60 + tdiff.days * 60 * 24)
390390
if not quiet:
391391
self.base.log("Invertor time {}, Predbat computer time {}, difference {} minutes".format(self.inverter_time, now_utc, tdiff))
392-
if abs(tdiff) >= 10:
392+
if abs(tdiff) >= 30:
393393
self.base.log(
394394
"Warn: Invertor time is {}, Predbat computer time {}, this is {} minutes skewed, Predbat may not function correctly, please fix this by updating your inverter time, checking HA is synchronising with your inverter, or fixing Predbat computer time zone".format(
395395
self.inverter_time, now_utc, tdiff
@@ -1457,8 +1457,7 @@ def write_and_poll_switch(self, name, entity_id, new_value):
14571457
self.base.call_service_wrapper(service, entity_id=entity_id)
14581458

14591459
self.sleep(self.inv_write_and_poll_sleep)
1460-
current_state = self.base.get_state_wrapper(entity_id=entity_id, refresh=True)
1461-
self.log("Switch {} is now {}".format(entity_id, current_state))
1460+
current_state = self.base.get_state_wrapper(entity_id=entity_id, refresh=domain != "sensor")
14621461
if isinstance(current_state, str):
14631462
current_state = current_state.lower() in ["on", "enable", "true"]
14641463

@@ -1503,7 +1502,7 @@ def write_and_poll_value(self, name, entity_id, new_value, fuzzy=0, ignore_fail=
15031502
return True
15041503

15051504
self.sleep(self.inv_write_and_poll_sleep)
1506-
current_state = self.base.get_state_wrapper(entity_id, refresh=True, required_unit=required_unit)
1505+
current_state = self.base.get_state_wrapper(entity_id, refresh=domain != "sensor", required_unit=required_unit)
15071506
if isinstance(new_value, str):
15081507
matched = current_state == new_value
15091508
else:

0 commit comments

Comments
 (0)