Skip to content

Solarbank1 schedule control and telemetry#27

Open
smariacher wants to merge 9 commits intoflip-dots:mainfrom
smariacher:solarbank1_control
Open

Solarbank1 schedule control and telemetry#27
smariacher wants to merge 9 commits intoflip-dots:mainfrom
smariacher:solarbank1_control

Conversation

@smariacher
Copy link
Copy Markdown
Contributor

Adds telemetry and schedule control to Anker Solarbank (1) E1600

Comment thread SolixBLE/devices/solarbank1.py
Comment thread SolixBLE/devices/solarbank1.py Outdated
Comment thread SolixBLE/devices/solarbank1.py Outdated
Comment thread SolixBLE/devices/solarbank1.py Outdated
Copy link
Copy Markdown
Owner

@flip-dots flip-dots left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 :)

@flip-dots flip-dots mentioned this pull request Mar 30, 2026
@smariacher
Copy link
Copy Markdown
Contributor Author

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 ChargingSchedule dataclass I created. I don't know if there is any other device that does it like the SB1, so putting ChargingSchedule into the solarbank1.py file seems like the best option to me, but you may disagree. Maybe there is another better option I don't know of.

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

@flip-dots
Copy link
Copy Markdown
Owner

@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
@thomluther
Copy link
Copy Markdown

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.

SB1 schedule format is definitely different to SB Gen 2 and later.
SB Gen 2 and later also have different plans, with different structures and Gen 3 introduced more plans, again with different structure. So it will be better to keep it with the device class I guess.

@flip-dots
Copy link
Copy Markdown
Owner

SB Gen 2 and later also have different plans, with different structures and Gen 3 introduced more plans, again with different structure. So it will be better to keep it with the device class I guess.

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?

@thomluther
Copy link
Copy Markdown

thomluther commented Apr 2, 2026

SB Gen 2 and later also have different plans, with different structures and Gen 3 introduced more plans, again with different structure. So it will be better to keep it with the device class I guess.

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.
From SB2 on, the normal time schedule slots only have a power value, not other values that SB1 still had per slot.
But SB2 can define different slot lists per weekday, which the SB1 did not support.

And SB2 and later comes with other plans with other structures:

  • Time of Use plan
  • Time Slot plan for dynamic tarifs
  • A backup charge schedule (start and end type and a switch)^

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.
I did not look further into plan extraction or modification through MQTT, since that is too complex and lots of guesswork.
The can be changed easily through the Api and the cloud will send the required MQTT commands to the device.
That of course does not work with BT connection only.

Comment thread SolixBLE/devices/solarbank1.py
Comment thread SolixBLE/devices/solarbank1.py
Comment thread SolixBLE/devices/solarbank1.py
Comment thread SolixBLE/devices/solarbank1.py
data = self._data["ae"]

# Safely extract the raw bytes
if isinstance(data, bytes):
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it sometimes bytes and sometimes a dictionary? It would be good to have tests for all cases if so.

Copy link
Copy Markdown
Owner

@flip-dots flip-dots left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@smariacher
Copy link
Copy Markdown
Contributor Author

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?

@flip-dots
Copy link
Copy Markdown
Owner

@smariacher sounds good. The best place for documenting quirks would be the docs/source/solarbank1.rst file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants