Solarbank1 schedule control and telemetry#27
Solarbank1 schedule control and telemetry#27smariacher wants to merge 9 commits intoflip-dots:mainfrom
Conversation
flip-dots
left a comment
There was a problem hiding this comment.
This is not bad, I have made a few changes, added it to the docs, added it to the API, and fixed some type issues. It would be good to have some tests added, like for testing the parsing of values, especially with how complex the schedule API is.
There are a few changes to the interface I would like to be made to make the library easier to use for others (like charging status, and the schedule), but for the most part its good.
Im not going to require it, since I dont have any existing tests for you to base it off, but it would be nice to also have a test or two for sending the schedule commands due to the complexity of it.
Writing tests is kind of a PITA, but its only so I don't break your code in future when inevitably end up refactoring stuff. Let me know if you need any help :)
|
Sorry for the long wait, I hopefully have all the things you mentioned fixxed and/or changed. Now the only thing which I am unsure about is where to put the new Here is the dataclass, maybe you have some comments here too. I implemented it in both the current_schedule property and the set_schedule function. @dataclass
class ChargingSchedule:
start_time: int
"""
Start of schedule in minutes since midnight.
"""
end_time: int
"""
End of schedule in minutes since midnight.
"""
output_wattage: int
max_soc : int
"""
Maximum SOC before Solarbank (presumably) goes into passthrough mode.
"""
def __str__(self) -> str:
"""Convert the integer minutes back to HH:MM format for a nice display"""
start_time_str = f"{self.start_time // 60:02d}:{self.start_time % 60:02d}"
end_time_str = f"{self.end_time // 60:02d}:{self.end_time % 60:02d}"
return (
f"Charging Schedule:\n"
f" Time: {start_time_str} - {end_time_str}\n"
f" Wattage: {self.output_wattage}W\n"
f" Max SOC: {self.max_soc}%"
)
def __post_init__(self):
MIN_WATTAGE, MAX_WATTAGE = 0, 800
MIN_SOC, MAX_SOC = 0, 100
if not (MIN_WATTAGE <= self.output_wattage <= MAX_WATTAGE):
raise ValueError(
f"Invalid output_wattage: {self.output_wattage}. "
f"Must be between {MIN_WATTAGE} and {MAX_WATTAGE}."
)
if not (MIN_SOC <= self.max_soc <= MAX_SOC):
raise ValueError(
f"Invalid max_soc: {self.max_soc}. "
f"Must be between {MIN_SOC} and {MAX_SOC}."
)
if not (self.end_time - self.start_time > 0):
raise ValueError(
f"Invalid time frame: Start: {self.start_time}, End: {self.end_time}. "
f"Start time must be smaller than end time."
)
if not (self.start_time >= 0 and self.start_time <= 1440):
raise ValueError(
f"Invalid start time: {self.start_time}. "
f"Start time cannot be less than 0 minutes or greater than 1440 minutes (24 hours)"
)
if not (self.end_time >= 0 and self.end_time <= 1440):
raise ValueError(
f"Invalid start time: {self.end_time}. "
f"End time cannot be less than 0 minutes or greater than 1440 minutes (24 hours)"
)
@classmethod
def from_time_strings(cls, start: str, end: str, output_wattage: int, max_soc: int) -> "ChargingSchedule":
"""Alternative constructor to create a schedule using HH:MM string formats."""
return cls(
start_time=cls.time_from_string(start),
end_time=cls.time_from_string(end),
output_wattage=output_wattage,
max_soc=max_soc
)
@staticmethod
def time_from_string(time: str) -> int:
"""
Converts a string time in 24-hour HH:MM format to minutes since midnight.
:param time: Time string in 24-hour HH:MM format.
:returns: Minutes since midnight.
"""
hours_str, minutes_str = time.split(":")
hours = int(hours_str)
minutes = int(minutes_str)
if hours > 24:
raise ValueError(f"Invalid hour value: {hours}. Hour must be between 0 and 24.")
if minutes > 59:
raise ValueError(f"Invalid minute value: {minutes}. Minute must be between 0 and 59.")
if hours == 24 and minutes != 0:
raise ValueError(f"Invalid time string: {time}. If hour is set to 24 then minutes may only be 0.")
return hours * 60 + minutes |
|
@smariacher its fine, I dont have the time to do a proper review until at least the weekend (but possibly later than that) due to upcoming coursework, but just taking a quick glance it looks good. I think its fine to leave ChargingSchedule in solarbank1.py for now, but if it ends up being identical to other models I will probably end up moving it to states.py. |
…anging schedule to use ChargingSchedule, used ChargingStatus inside charging_status property instead of int
SB1 schedule format is definitely different to SB Gen 2 and later. |
Is it just the bytes that are different or is the abstract structure of starting at A time, ending at B time with output power C, and max SoC D different for others as well? |
Different content. And SB2 and later comes with other plans with other structures:
To make it even more complex, what I have seen in MQTT commands is that adjusting the schedules uses the same command message and same command fields, but completely different structures for the fields, depending on the plan type contained in the command. I have no idea where those plans end up in the telemetry data. Could be that query of the plan requires another specific command that is part of the normal telemetry data. |
| data = self._data["ae"] | ||
|
|
||
| # Safely extract the raw bytes | ||
| if isinstance(data, bytes): |
There was a problem hiding this comment.
Is it sometimes bytes and sometimes a dictionary? It would be good to have tests for all cases if so.
flip-dots
left a comment
There was a problem hiding this comment.
Its looking much better now, I think it mostly just needs some tests so I don't end up breaking it in future as I will probably end up having to move some stuff around.
|
Quick update since I haven't posted much for a while: I changed the code according to your feedback and wanted to test things one last time before commiting, but right now the weather is so bad that my PV modules don't produce any power making testing (e.g. charging/discharging status, (rapid) schedule changing etc.) I would also like to submit some info about the quirks the SB1 brings with it. Stuff like minimum wattage output when using BLE vs cloud, battery output when solar input dips below the currently set schedule, wake up time for the inverters and so on. Where exactly would you like me to put this information @flip-dots? |
|
@smariacher sounds good. The best place for documenting quirks would be the docs/source/solarbank1.rst file. |
Adds telemetry and schedule control to Anker Solarbank (1) E1600