Write charge/discharge schedules to the periodic API as well - #270
Conversation
Some regions have moved to a backend that only acts on setTimeChargeBySn. There the legacy updateChargeConfigInfo call still returns 200 but the inverter never applies it until you press Save in the app, or SetConfig times out. Other regions still only understand the legacy endpoints, so write both: periodic first, legacy after. Support is probed once per inverter on setup; systems that aren't entitled return 6017 and keep using the legacy endpoints only. The write payload was being rebuilt in time.py, switch.py and number.py, so move it onto the coordinator and have the buttons and services use it too. Fixes CharlesGillanders#267 Fixes CharlesGillanders#269
The switch writes are logged by the coordinator now, so _LOGGER here was assigned but never read.
Shows whether an inverter is on the newer periodic backend or only accepts the legacy endpoints, which is the first thing worth knowing on any report of charge/discharge times not applying. Also retry the probe on full polls while the answer is still unknown, so a cloud hiccup during setup doesn't leave the sensor stuck, and include the value in the downloadable diagnostics.
getTimeChargeBySn is separately permissioned from setTimeChargeBySn and returns 6017 on accounts whose systems are on the new backend, so using it to decide whether to write the periodic schedule skipped the write for exactly the users CharlesGillanders#267 and CharlesGillanders#269 are about. Write both APIs unconditionally instead. A failed periodic write now falls through to the legacy one rather than aborting, and a change is only reported as failed when neither write got through. The diagnostic sensor can only speak for the read, so rename it to Periodic Schedule Read with readable/unreadable/unknown and say plainly that unreadable does not mean the system is on the old backend.
Verified against the live API: an empty chargeTimeList/dischargeTimeList is rejected with 6001 "time list is null", and omitting one gives 10001. Both lists must contain at least one period, and there is no confirmed way to say "no periods on this side" — a 00:00-00:00 element passes validation but its meaning is unverified, and would be a 24-hour window if the server reads end <= start as wrapping midnight. So skip the periodic write when either list would be empty and log why. The legacy endpoints still run. This also means Reset Charge/Discharge cannot clear a periodic schedule, which the README now says.
|
Yes, I need this for my system. Happy to help with testing / coding. |
|
and code reviews for that matter. |
|
Heya, yeah I'm almost done, I'm just testing with my inverter atm as there seems to be a few quirks and return codes that are unique when you haven't set "time based charging" up at all |
|
Oh shame, I just started poking at it tonight to try and get it to work! |
|
Got an upstream bump to do before this can merge: CharlesGillanders/alphaess-openAPI#29 |
alphaessopenapi 0.0.21 can surface the code behind a failure instead of returning None for everything, so turn it on and act on it. The periodic write is the reason for this. It used to be impossible to tell an accepted schedule from a rejected one, because setTimeChargeBySn answers with data: null either way. Now a rejection arrives as AlphaESSApiError, so a 6017 means we know this system isn't entitled to the periodic API and can stop asking on every write. The legacy endpoints still get written as before. The flag applies to reads too, which would have been a regression: an endpoint the account can't use would have raised mid-fetch, aborted the whole inverter, and after three polls tripped the backoff and dropped it entirely. Polled reads now go through _read, which absorbs an API-level rejection and carries on with that one value missing, exactly as they behaved before. Transport errors still propagate — those really do mean the inverter is unreachable. Also handles the case where getESSList is refused during setup, which used to be an unhandled traceback; credential-shaped codes raise ConfigEntryAuthFailed and the rest ConfigEntryNotReady.
Turning on raise_on_error meant several paths that quietly received None now receive an exception instead, and most of them were not ready for it. Switch and number writes left their optimistic value on screen when the write failed, so the UI claimed a setting had been applied when nothing had. Both now revert, the way the time entities already did. Number puts the stored value back too, since that is what the charge/discharge buttons send next. Buttons took their 30 second rate limit slot before the call and kept it even when the command was refused, so a rejected press cost the user half a minute before they could retry. The timestamp is now restored on failure and the notification says what happened rather than claiming success. The two services raised the library exception straight at the user; they now raise HomeAssistantError with a readable message. Config flow gets raise_on_error too, which fixes a pre-existing bug: a wrong AppSecret returns 6007 with an empty body, which used to read as "no systems" and still created the entry. It now fails validation as invalid auth. Narrowed AUTH_FAILURE_CODES to codes re-entering credentials can actually fix. 6005 (appId not bound to that SN) and 6009 (IP whitelist) are portal-side configuration problems, and prompting for reauth would have looped the user.
Four of the periodic log lines printed a bare code — "(6017)", "6042" — which are the ones most likely to end up pasted into an issue, and the least useful without the meaning next to them. describe_api_error renders them consistently from the description the library already attaches to the exception, so the wording keeps coming from RETURN_CODES / UNDOCUMENTED_RETURN_CODES upstream and isn't copied into this repo. Codes the library doesn't know still show their number.
async_set_native_value stores the new value before pushing it, so when the push failed the rejected value stayed behind — and that stored value is what the charge/discharge buttons send next time. Reverting it is not quite enough either. get_number_setting only falls back to its default when the key is missing, so writing None back would shadow the 90/10 defaults and hand None to the API. When there was no previous value the key is now dropped instead, via clear_number_setting.
|
Bump @CharlesGillanders @Swamp-Ig im going to release this as a beta so i can see how the implementation is for regions outside of Australia, and regions still using the "old" server (i think china still does) then make any changes as needed. as im still not 100% confident in my implementation |
|
@Poshy163 I'm in Australia too, so I expect it will be pretty similar, but will do a code review over the weekend and give it a try. |
Some regions have moved to a backend that only acts on setTimeChargeBySn. There the legacy updateChargeConfigInfo call still returns 200 but the inverter never applies it until you press Save in the app, or SetConfig times out. Other regions still only understand the legacy endpoints, so write both: periodic first, legacy after.
Support is probed once per inverter on setup; systems that aren't entitled return 6017 and keep using the legacy endpoints only.
The write payload was being rebuilt in time.py, switch.py and number.py, so move it onto the coordinator and have the buttons and services use it too.
Fixes #267
Fixes #269