Skip to content
Merged
3 changes: 3 additions & 0 deletions apps/predbat/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@ def load_component_class(component_info):
"inverter_limit": {"required": False, "config": "hanchu_inverter_limit"},
"api_delay": {"required": False, "default": 1.0, "config": "hanchu_api_delay"},
"min_write_interval": {"required": False, "default": 60, "config": "hanchu_min_write_interval"},
# Logs every API request and response. Off by default: on, it writes thousands of long
# lines a day per device. For a tester proving what hanchu_const.py marks INFERRED.
"api_debug": {"required": False, "default": False, "config": "hanchu_api_debug"},
},
# Gate activation on having an account. Without this the component would start for every
# instance, since all individual args are optional.
Expand Down
6 changes: 5 additions & 1 deletion apps/predbat/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2395,7 +2395,10 @@
"write_and_poll_sleep": 2,
"has_time_window": False,
"support_charge_freeze": True,
"support_discharge_freeze": True,
# CHG_PWR_LMT only limits charging from the grid, so a zero charge rate leaves solar filling
# the battery and Freeze Export cannot be carried out (batpred#5665). "Maximum Charge SOC" could
# do it, but only from 50% up and only with a restore the component does not have.
"support_discharge_freeze": False,
"has_idle_time": False,
# Wrap-around behaviour is undocumented for the TCT_/TDT_ slot pairs - as is everything
# else about this API - so Predbat splits the window and slot 2 carries the remainder
Expand Down Expand Up @@ -2880,6 +2883,7 @@
"hanchu_inverter_limit": {"type": "float"},
"hanchu_api_delay": {"type": "float"},
"hanchu_min_write_interval": {"type": "integer"},
"hanchu_api_debug": {"type": "boolean"},
"alphaess_app_id": {"type": "string", "empty": False},
"alphaess_app_secret": {"type": "string", "empty": False},
"alphaess_inverter_sn": {"type": "string|string_list", "empty": False},
Expand Down
306 changes: 254 additions & 52 deletions apps/predbat/hanchu.py

Large diffs are not rendered by default.

65 changes: 63 additions & 2 deletions apps/predbat/hanchu_const.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,53 @@
# discovery catalogue as an account id.
HANCHU_STATION_FIELD = "stationId"

# HARDWARE (batpred#5664): getDeviceStatus carries the model as a code, which the Hanchu web
# console's own pcsEnMap turns into a name - devModel 4 is a HESS-HY-S-5.0K, confirmed on that unit.
# The device-list entry carries no model at all ({"devType", "sn"} only).
HANCHU_MODEL_FIELD = "devModel"

# devModel -> (model, battery charge/discharge limit W, AC rating W), so battery_rate_max and
# inverter_limit come from the unit rather than from a power setting the menu shares across models
# (batpred#5664). Names from the console's pcsEnMap; ratings from the Hanchu user manuals, both as
# reported on batpred#5664. The codes are not in size order - 30-32 were added later - so this is a
# lookup, never an ordinal. A battery limit of None is one the manual does not state (the 48 V,
# current-limited S-6K-S and S-12K-S): battery_rate_max then falls back to the menu and held values,
# as it does for a code missing here altogether (the HC-HY-SL-*-G2 range the menu also lists).
HANCHU_MODELS = {
1: ("HESS-HY-S-3.0K", 5000, 3000),
2: ("HESS-HY-S-3.68K", 5000, 3680),
3: ("HESS-HY-S-4.0K", 5000, 4000),
4: ("HESS-HY-S-5.0K", 5000, 5000),
5: ("HESS-HY-S-6.0K", 5000, 6000),
6: ("HESS-HY-T-05K", 5000, 5000),
7: ("HESS-HY-T-06K", 6000, 6000),
8: ("HESS-HY-T-08K", 8000, 8000),
9: ("HESS-HY-T-10K", 10000, 10000),
10: ("HESS-HY-T-12K", 12000, 12000),
11: ("HESS-HY-T-4K-M", 4000, 4000),
12: ("HESS-HY-T-5K-M", 5000, 5000),
13: ("HESS-HY-T-6K-M", 6000, 6000),
14: ("HESS-HY-T-8K-M", 8000, 8000),
15: ("HESS-HY-T-10K-M", 10000, 10000),
16: ("HESS-HY-T-12K-M", 12000, 12000),
17: ("HESS-HY-T-10K-M4", 10000, 10000),
18: ("HESS-HY-T-12K-M4", 12000, 12000),
19: ("HESS-HY-T-15K-M4", 15000, 15000),
20: ("HESS-HY-T-20K-M4", 20000, 20000),
21: ("CESS-HY-25K", 25000, 25000),
22: ("CESS-HY-30K", 30000, 30000),
23: ("CESS-HY-36K", 36000, 36000),
24: ("CESS-HY-40K", 40000, 40000),
25: ("CESS-HY-50K", 50000, 50000),
26: ("CESS-HY-40K-P", 40000, 40000),
27: ("CESS-HY-50K-P", 50000, 50000),
28: ("HESS-HY-S-6K-S", None, 6000),
29: ("HESS-HY-S-12K-S", None, 12000),
30: ("HESS-HY-S-7.0K", 7000, 7000),
31: ("HESS-HY-S-8.0K", 8000, 8000),
32: ("HESS-HY-S-10.0K", 10000, 10000),
}

# --- Daily energy: getDeviceStatistics --------------------------------------------------------
# Predbat sensor leaf -> getDeviceStatistics field. All OBSERVED in UPTON sensor.py
# STATISTICS_SENSORS; GUOXIA sensor.py carries the same six names, so all six are CORROBORATED.
Expand Down Expand Up @@ -443,11 +490,23 @@
# giving up and leaving the staged changes intact for the user to retry.
HANCHU_WRITE_RETRY_DELAY = 5

# Seconds a write-button press that arrived on another event loop waits for the write it handed to
# the component's own loop (see HanchuAPI._on_component_loop). Above one write's worst case - three
# 15 s attempts with their back-off, an inline retry after HANCHU_WRITE_RETRY_DELAY and a re-login -
# so a slow cloud is waited out, while a stuck loop cannot hold Predbat's plan thread indefinitely.
# Giving up only stops the waiting: the write carries on.
HANCHU_LOOP_HANDOVER_TIMEOUT = 120
Comment thread
springfall2008 marked this conversation as resolved.

# A write that FAILED is retried on the next cycle rather than held for the full pacing interval:
# a rejected shorten leaves the inverter running past the plan, so waiting costs real energy. The
# reconcile loop runs about once a minute, so this still means at most one attempt per cycle.
HANCHU_FAILED_WRITE_INTERVAL = 30

# A control read-back is compared with what Predbat last wrote only when the read began at least
# this long after that write, so a read the write may have overtaken is not taken for a change made
# on the device. HARDWARE (batpred#5305): an accepted write reads back within 1-2 s; a minute is margin.
HANCHU_READBACK_SETTLE = 60

# HARDWARE (batpred#5305): after this many consecutive rejected writes the user is notified,
# because the inverter may no longer match the plan and may need stepping in on by hand.
HANCHU_WRITE_FAILURE_NOTIFY = 3
Expand Down Expand Up @@ -501,7 +560,9 @@ def split_window_seconds(start, end):

INVERTER_DEF sets can_span_midnight False, so Predbat itself avoids asking for a wrap-around,
but it still expresses "until midnight" as an end hour of 24 or more. The second pair carries
whatever runs past midnight and is (0, 0) - disabled - when nothing does.
whatever runs past midnight and is (0, 0) - disabled - when nothing does. An end of exactly
24:00:00 is clamped to 23:59 like hhmmss_to_seconds(), because 86400 is outside the slot range
and one out-of-range key fails the whole batched iotSet (batpred#5672).

Returns ((start1, end1), (start2, end2)).
"""
Expand All @@ -512,7 +573,7 @@ def split_window_seconds(start, end):
end_seconds = int(parts[0]) * 3600 + (int(parts[1]) * 60 if len(parts) > 1 else 0)
except (TypeError, ValueError):
end_seconds = start_seconds
if end_seconds <= HANCHU_SECONDS_PER_DAY:
if end_seconds < HANCHU_SECONDS_PER_DAY:
return (start_seconds, end_seconds), (HANCHU_SLOT_DISABLED, HANCHU_SLOT_DISABLED)
return (start_seconds, HANCHU_SECONDS_PER_DAY - 60), (HANCHU_SLOT_DISABLED, end_seconds - HANCHU_SECONDS_PER_DAY)

Expand Down
Loading
Loading