Skip to content

Solarbank 2 Schedule/Power control - #36

Open
jul1an-s wants to merge 18 commits into
flip-dots:mainfrom
jul1an-s:feature/solarbank2_control
Open

Solarbank 2 Schedule/Power control#36
jul1an-s wants to merge 18 commits into
flip-dots:mainfrom
jul1an-s:feature/solarbank2_control

Conversation

@jul1an-s

@jul1an-s jul1an-s commented May 20, 2026

Copy link
Copy Markdown

Changes:

  • Added Prime style handshake to SB2
  • Controls:
    • Added the code to send simple schedules to SB2 405e. Currently restricted to setting one power output 24/7
    • Added boilerplate for additional Commands [untested]:
      • light control
      • Added Reserved Power control
      • Added max output power control
  • Will add a frida script for dart lib as well
  • Extended gitignore

Tests & Docu

  • Docu updated
  • Added a few new tests
  • Tests passed

Why two handshakes?

As I mentioned in the issue #28 I have implemented both handshakes/protocol variants:

Prime handshake

I included the Prime style variant because it seems like the "right" way to interact with the device and a lot of work went into it. The legacy handshake might get removed any time, so it's good to have a fallback option.

"Legacy" handshake

I included the Legacy variant because right now it is so much simpler for our use case. No env vars, no user specific config.

@jul1an-s
jul1an-s marked this pull request as ready for review May 20, 2026 10:41
jul1an-s pushed a commit to jul1an-s/HaSolixBLE that referenced this pull request May 21, 2026
- Exposes SolixBLE set_schedule(power_w) flip-dots/SolixBLE#36 as a user-facing control.
- Number entity (0-800 W, 10 W step, slider) stages the target value;
- Apply button to commit it to the device.
- Last value restored across HA restarts via RestoreEntity.
- Library errors surface as HomeAssistantError so they show as UI notifications.
- No Version increment yet -> Maintainer
- Add Solarbank 2 to README supported devices list
- Added unit tests
Comment thread SolixBLE/devices/solarbank2.py Outdated
async def _initiate_negotiations(self) -> None:
"""Start the legacy base SolixBLEDevice handshake."""
_LOGGER.info("SB2: starting legacy base handshake (00xx/08xx, AES-CBC)")
await super()._initiate_negotiations()

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.

I think it makes sense to cut this since _initiate_negotiations() already produces a log entry when called.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This log output is meant to let the reader now whether legacy or prime style handshake was used for the connection. _initiate_negotiations() does not provide this information. Is it ok to leave it in there for now?

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.

Sure, but maybe don't call the old style "legacy" just yet given that the new one is very experimental. I think its only fair to call it legacy when the newer style is able to fully supersede it (including being able to generate these IDs).


# Encryption helpers

def _encrypt_with_static_key(self, plaintext: bytes) -> 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.

When I originally implemented the prime protocol I think I just stored the cipher text in constants instead of storing the plain text and then encrypting it before sending it since they were constants, but since you now need that functionality I think it would make more sense to implement it in the _encrypt_payload() function of prime_device.py.

In _decrypt_payload() it automatically determines if it should use the static or dynamic key depending upon if the dynamic key is set, you should be able to use that to implement something similar for _encrypt_payload()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Does this still hold true with all the new developments in the other devices? If yes, I'll move it. Please let me know.

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.

It holds true for prime devices, the older style Solix devices do not use encryption with a static key for the first few messages, those are all in plain text.

Comment thread SolixBLE/devices/solarbank2.py
Comment thread SolixBLE/devices/solarbank2.py
Comment thread SolixBLE/devices/solarbank2.py
Comment thread tests/test_devices.py Outdated
Comment thread tests/test_devices.py Outdated
Comment thread tests/test_devices.py Outdated
Comment thread scripts/frida_sb2_dart.js Outdated
Comment thread docs/source/app_decoding.rst
Comment thread SolixBLE/devices/solarbank2.py
.. note::
A pristine, never-paired SB2 may still require a one-time pairing
through the Anker app before any BLE client (including SolixBLE)
can connect.

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.

Apologies if this is already explained elsewhere and I just missed it, but how would one go about getting this cloud ID, does the app show it anywhere?, if you use the legacy protocol is it included in the telemetry data? or might there be some command in the legacy protocol to request it that could be found in the dumps?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

So the ideal workflow for the future is:

  • We reverse engineer the pairing process (not the handshake, the actual first time pairing with the button press).
  • I am 99% sure this id is transferred during pairing
  • We implement our own pairing in SolixBLE
  • User can set whatever ID he/she pleases

In the meantime maybe it's possible to get the ID from the cloud API, but I haven't tried because I knew mine from the captures


Uses 40xx/48xx negotiation across 8 stages and AES-GCM for session
traffic. Requires an Anker user-id. SB2 firmware whitelists user-ids
and rejects unknown values with RX 4827 = ``09 a1 02 b4 00``.

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.

It wouldn’t happen to accept a 0000000 user ID would it? Its probably wishful thinking but we have seen Anker take a few shortcuts when it comes to security so I wouldn’t be completely surprised.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

No it rejects a random user ID. But as I said above, reverse engineering the pairing process is probably very easy now. I simply ran out of time

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.

Fair enough. Given that this needs someone to go through the effort of finding the ID I think this needs to remain experimental for now. I am fine with merging it but it needs to be clearly labelled in the docs as experimental.

Comment thread SolixBLE/devices/solarbank2.py
Comment thread SolixBLE/devices/solarbank2.py Outdated
Comment thread SolixBLE/devices/solarbank2.py

@flip-dots flip-dots left a comment

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.

You know I gotta hand it to you, this is some great work, its highlighted quite a few shortcomings with the existing abstractions I made which I should probably fix at some point, hell most of my review comments are just me highlighting things for future me to do.

In theory my availability should be a lot better now that my university exams are sorted so feel free to reach out if you need additional clarification or some help with this.

@jul1an-s

jul1an-s commented Jun 23, 2026

Copy link
Copy Markdown
Author

Thank you so much for taking your time to review this big blob so thoroughly. I appreciate it a lot!

I will consider every one of your comments, I just replied to the obvious ones where I don't need to change much on my side.

With that being said my current availability is very poor for the next few months, so I don't know when I'll find the time to work on this again. If someone else wants to pick this up in the meantime I'm fine with it and I'm happy to help.

@jul1an-s

jul1an-s commented Jul 5, 2026

Copy link
Copy Markdown
Author

Noticed a bug today in "production":

Device reports input_cutoff_data = 100 %

Possible causes:

  • Maybe due to latest firmware update of the device, which now uses input_cutoff_data properly (seemed to be always 5 or 10 in the past)
  • I triggered this behavior by changing the output_cutoff_data using my set_reserved_power() function.

Either way this crashes the HA integration. Needs fix before merge @jul1an-s .

Error message in HA
Logger: homeassistant.components.sensor
Source: helpers/entity_platform.py:455
integration: Sensor (documentation, issues)
First occurred: 09:02:41 (1 occurrence)
Last logged: 09:02:41

Error while setting up solix_ble platform for sensor: 100 is not a valid SBPowerCutoff
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/entity_platform.py", line 455, in _async_setup_platform
    await asyncio.shield(awaitable)
  File "/config/custom_components/solix_ble/sensor.py", line 200, in async_setup_entry
    SolixSensorEntity(
    ~~~~~~~~~~~~~~~~~^
        device,
        ^^^^^^^
    ...<5 lines>...
        state_class=None,
        ^^^^^^^^^^^^^^^^^
    )
    ^
  File "/config/custom_components/solix_ble/sensor.py", line 1117, in __init__
    self._update_updatable_attributes()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File "/config/custom_components/solix_ble/sensor.py", line 1131, in _update_updatable_attributes
    attribute_value = getattr(self._device, self._attribute_name)
  File "/usr/local/lib/python3.14/site-packages/SolixBLE/devices/solarbank2.py", line 695, in input_cutoff_data
    return SBPowerCutoff(self._parse_int("b6", begin=1))
  File "/usr/local/lib/python3.14/enum.py", line 707, in __call__
    return cls.__new__(cls, value)
           ~~~~~~~~~~~^^^^^^^^^^^^
  File "/usr/local/lib/python3.14/enum.py", line 1192, in __new__
    raise ve_exc
ValueError: 100 is not a valid SBPowerCutoff

@thomluther

Copy link
Copy Markdown

The Input cutoff field is now used as max soc, but only if the device firmware supports it.

@jul1an-s

jul1an-s commented Jul 6, 2026

Copy link
Copy Markdown
Author

Hi @thomluther,

thank you for sharing. Could you elaborate a little further? The behavior of the field is not clear to me:

  1. Are you talking about telemetry field b6 or TX field a4 of CMD 4067 or both?
  2. b6 was 10% at the beginnning. I submitted CMD 4067 with a4 = 5% During the next hours I received b6 = 5%. The next morning I received b6 = 100 and after a HA reboot I received b6 =5% again. Do you have explanation for this behavior?

@thomluther

thomluther commented Jul 7, 2026

Copy link
Copy Markdown
  1. Are you talking about telemetry field b6 or TX field a4 of CMD 4067 or both?

This is the old SOC reserve command structure (4067 or MQTT 0067):

    "a2": {
        NAME: "set_output_cutoff_data",  # 10 | 5 %
        TYPE: DeviceHexDataTypes.ui.value,
        STATE_NAME: "output_cutoff_data",
        VALUE_OPTIONS: [5, 10],
    },
    "a3": {
        NAME: "set_lowpower_input_data",  # 5 | 4 %
        TYPE: DeviceHexDataTypes.ui.value,
        STATE_NAME: "lowpower_input_data",
        VALUE_FOLLOWS: "set_output_cutoff_data",
        VALUE_OPTIONS: {5: 4, 10: 5},
    },
    "a4": {
        NAME: "set_input_cutoff_data",  # 10 | 5 %
        TYPE: DeviceHexDataTypes.ui.value,
        STATE_NAME: "input_cutoff_data",
        VALUE_FOLLOWS: "set_output_cutoff_data",
        VALUE_OPTIONS: {5: 5, 10: 10},
    },

Those 3 fields had only 2 options each, and their state was in 0405 messages:

            "b4": {NAME: "output_cutoff_data"},
            "b5": {NAME: "lowpower_input_data"},
            "b6": {NAME: "input_cutoff_data"},

I'm not sure what that field was used for, it was inherited from Solarbank 1. Maybe it was always ignored.

The new SOC limit command structure uses different fields except a2 (4067 or MQTT 0067):

    "a2": {
        NAME: "set_min_soc",
        TYPE: DeviceHexDataTypes.ui.value,
        STATE_NAME: "min_soc",
        VALUE_MIN: 5,  # 5 % for SB2, 1 %  for SB3
        VALUE_MAX: 20,
        VALUE_STATE: "min_soc",
    },
    "a5": {
        NAME: "set_max_soc",
        TYPE: DeviceHexDataTypes.ui.value,
        STATE_NAME: "max_soc",
        VALUE_MIN: 80,
        VALUE_MAX: 100,
        VALUE_STATE: "max_soc",
    },
    "a6": {
        NAME: "set_backup_soc",
        TYPE: DeviceHexDataTypes.ui.value,
        STATE_NAME: "backup_soc",
        VALUE_MIN: 0,
        VALUE_MAX: 99,
        # ensure backup is soc_min < backup < soc_max
    },
    "a7": {
        NAME: "set_backup_soc_switch",
        TYPE: DeviceHexDataTypes.ui.value,
        STATE_NAME: "backup_soc_switch",
        VALUE_OPTIONS: {"off": 0, "on": 1},
        VALUE_STATE: "backup_soc_switch",
    },

The output_cutoff_data b4 field was always the SOC reserve, and is now also used as new min SOC value. The b6 field is presenting the used charge SOC limit, that is typically identical with the max SOC. The eb field is new and shows the max SOC setting from the command:

    "b4": {NAME: "min_soc"},
    "b5": {NAME: "lowpower_input_data"},
    "b6": {NAME: "active_charge_soc"},
    "eb": {NAME: "max_soc"},

It could also be that the original field always had the same meaning 'input_cutoff_data' => limit for the charge, but maybe this is ignored by the firmware, if not larger than min soc, or 'output_cutoff_data', this the battery was always charged to 100%, since the old SOC reserve command always set this field the same value as the minimum...

Anyway, you need to find a way to recognize from the installed firmware level or 'eb' field value, whether the old or the new SOC command structure should be used. The old command should not be used anymore with the newer firmware, since it may invalidate the max soc setting.

I should also note that Solarbank 2 does not support the backup soc or switch settings to my knowledge, but the App always sends all 4 fields, where the 2 backup fields are always 0. Not sure if they are really needed in the command structure.

@jul1an-s
jul1an-s requested a review from flip-dots July 31, 2026 14:48
@jul1an-s

jul1an-s commented Jul 31, 2026

Copy link
Copy Markdown
Author

@flip-dots I think we're ready to merge. No need to prolong this any longer.

I will tackle the input cutoff problem later when I have time and I'm planning to rework the set power to make it clear we are writing a schedule there.
But this will be another PR.

@flip-dots flip-dots left a comment

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.

There are a few things that I would like to see addressed, most importantly the statically defined time zone, though there is some code which might help you there, that and the bits of code from the solarbank1 that might be compatible which could simplify things a lot.

Its worth noting that there are a few things in progress at the moment which solve a few of the problems you have had to solve yourself (timestamps, time-zones, negotiation) and you might want to wait for those to get merged before continuing on (though I don't mind if you don't want to wait).

async def _initiate_negotiations(self) -> None:
"""Start the legacy base SolixBLEDevice handshake."""
_LOGGER.info("SB2: starting legacy base handshake (00xx/08xx, AES-CBC)")
await super()._initiate_negotiations()

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.

Sure, but maybe don't call the old style "legacy" just yet given that the new one is very experimental. I think its only fair to call it legacy when the newer style is able to fully supersede it (including being able to generate these IDs).


Uses 40xx/48xx negotiation across 8 stages and AES-GCM for session
traffic. Requires an Anker user-id. SB2 firmware whitelists user-ids
and rejects unknown values with RX 4827 = ``09 a1 02 b4 00``.

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.

Fair enough. Given that this needs someone to go through the effort of finding the ID I think this needs to remain experimental for now. I am fine with merging it but it needs to be clearly labelled in the docs as experimental.


# Encryption helpers

def _encrypt_with_static_key(self, plaintext: bytes) -> 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.

It holds true for prime devices, the older style Solix devices do not use encryption with a static key for the first few messages, those are all in plain text.

# The tz_offset is signed LE seconds; CEST in capture was -7200
# (= -2h). We hardcode that for now - proper localtime detection
# is a TODO.
tz_str = b"CET-1CEST,M3.5.0,M10.5.0/3"

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.

I think this is too important to not implement. #45 changes how negotiations work so the time is correct for other devices where that matters (e.g chargers/power banks with the built in clock) and there is some code in that for generating time zones that might be useful.

You could also wait for that code (or something similar which fixes times) to be merged since its a relatively high priority and I will probably end up addressing it soon.

# is a TODO.
tz_str = b"CET-1CEST,M3.5.0,M10.5.0/3"
tz_offset = (-7200).to_bytes(4, "little", signed=True)
pt = (

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.

Does the timestamp need to be at the beginning of the packet rather than the end?, usually its at the end. If it works with it at the end you can probably use _send_command() to have it auto-generated instead of needing to generate it for all of these.

"""
return bytes.fromhex(f"a10121a2020100a30201{0 if light_on else 1:02x}")

async def set_light_switch(self, light_on: bool) -> None:

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.

For these kinds of things the rest of the library uses dedicated turn_off and turn_on functions (e.g here) it would be best to stick to that convention.

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.

That and it makes it easier to add to the Home Assistant integration since it expects switches to work that way.

)

# Mapping for 3rd field reserved power command
_RESERVED_POWER_A3_MAP: dict[int, int] = {5: 4, 10: 5}

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.

Why put it here instead of at the top of the file with the rest of the constants?

# 0x405e set-schedule

@staticmethod
def _build_set_schedule_payload(power_w: int) -> 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.

As per this, if you are able to use the FamilyLoadSchedule from #27 that would be really good. If not then it would be good for this code to use the same dataclass structure as is used there. I feel like this is just a tad too complicated to put in the device class and it would also be easier to test and document if it was separate.

# Decrypt for logging; _decrypt_payload picks static vs session key
# automatically based on whether _shared_secret has been set.
try:
decrypted = self._decrypt_payload(payload)

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 this needed? _decrypt_payload() should automatically determine when to use the static/session key based on if the session key variable is None or not.

@jul1an-s

jul1an-s commented Aug 3, 2026

Copy link
Copy Markdown
Author

Yeah I am aware that there are a lot of things going on in this project right now which make this lib pretty much a moving target.
That's why I wanted to get this merged before the entire core changes and forces me to redo most of this PR.

Honestly I'm not sure if my availability for this project allows me to get this merged given that some people appear to be working at full auto-vibe code speed.
I want keep testing every change I do E2E with the device and also review every line before release. But this does of course take time and will always be slower than an automated process. Especially with a job and other things to do as well.

I can try to work on your latest comments soon to get this merged before the great refactor. But if I need to rework and retest the entire inner machinery I guess I'll just leave it to someone else with better availability.
We could also get this merged and have the refactor load on the AI assistants if you plan to accept this kind of contribution for this project. It won't be much overhead on their side.

Please let me know how you want to proceed

@flip-dots

Copy link
Copy Markdown
Owner

I 100% get it, its really easy to get drawn into something and then find out its a massive hassle with a seemingly infinite number of change requests and a moving target, I experienced the exact same thing when I added support for Bluetooth Hue lights to Home Assistant, first it was the tests, then its the layout/structure, then they deprecated an API I used, and then they added all these requirements for 3rd party libraries to be built with CI/CD and be auditable and it was a total nightmare, I think it ended up taking over a year for all of it to go through and even now there are still issues with it.

There is a good reason for it though, as the main developer of this thing I am taking on responsibility for maintaining it which means I need to be able to understand and maintain it, as well as anyone else who might need to change it and I don't have the hardware to test, so making unilateral changes later to things like packet decoding and negotiation is quite risky, tests can help mitigate most of it but not all.

In terms of the more practical aspects I am hoping some of these soon to be implemented changes (like timestamps and timezones being handled by the base classes) will be able to significantly shrink this PR which shrinks the amount of code that I have to maintain that I am not able to test on hardware. I don't expect the big PRs that change a lot of the things to be merged soon, there are many issues with them as it stands, I suspect I will end up making my own which implement just the minimum. In terms of timescales I am happy to leave this PR as draft for as long as needed and I might end up building something to implement timestamps and timezones for all devices in the next few days since I now have a device where the time actually matters (one of those desktop chargers which has a clock).

If your availability does not allow for you to sort all of this out yourself I don't mind implementing the improvements myself and I could probably do that some time in the next month or two after the timestamp/time-zone changes are done, so long as you are willing to test it and make sure it works and send me any test data I might need.

@jul1an-s

jul1an-s commented Aug 4, 2026

Copy link
Copy Markdown
Author

I totally understand that you as a maintainer have a great interest in keeping the codebase lean and maintainable.

I'm sorry that you had so much hassle with your HA project :-( However I can understand them having thousands of users and hundreds of contributors. A project of that size needs this kind of thoroughness (especially given that they are doing very critical things like door locks etc).
It really depends on where you want to go with this project. Imo this started as a damn cool POC with a hardcoded private key and lot's of device templates which were simple placeholders without any use (no offense, I would have done the same thing and this was exactly the right approach 🙂 ).
Of course it's up to you to decide and I understand your motives.

Nevertheless you should consider two aspects:

  1. We don't even know whether the Anker protocol allows this level of generalization of the protocol. From what I've seen, each device seems to do whatever it pleases and a common handshake might never work across all devices. Actually Anker might need an architect like you 😉 Whenever you change the core of this lib, you will need to retest EVERY device that relies on it. I know you heavily rely on automated testing which is good, but it only gets you so far.
  2. It's just code. It's not set in stone. You can roll back every PR, you can remove any device implementation you don't like at any time.

Regarding the size of this PR we could simply drop the frida_dart script which is 1/3 of the PR and the "SolarbankPrime" path which is probably another 1/3 and not wired to your HA repo anyways. I checked it in to document my findings for future contributors, but ofc it introduces maintenance overhead for you. It can also rest on my harddrive like other stuff I discovered which is not part of this PR (OTA flashing etc)

Sure we can leave this open if you have time to work on it in the close future, but I really wanted to ship this to "normal" users in HA soon given some comments in the HA repo which suggest this is already being used by some people 🤔 Especially with Anker having officially denied all hopes for an official solution for this device.

@MrChurch

MrChurch commented Aug 9, 2026

Copy link
Copy Markdown

Hi Julius,

we have now integrated an owned Solarbank 2 E1600 AC (A17C0) locally via BLE. Your dynamic/Prime-style handshake work was very helpful, and a few observations from our captures may be useful for your PR:

  • The complete observed sequence is:
    4001 -> 4801 -> 4003 -> 4803 -> 4029 -> 4829 -> 4005 -> 4805 -> 4021 -> 4821 -> 4022 -> 4822 -> 4027 -> 4827 -> 4040

  • 4005/4805 was required before the P-256 ECDH exchange on our device.

  • The bootstrap requests 4001, 4003, 4029, and 4005 reused one timestamp. Your implementation already does this; our capture confirms that behaviour.

  • The replies to 4022 and 4027 arrived as AES-GCM session frames with pattern 03000f, not 030001:
    RX 4822 (03000f) -> TX 4027 -> RX 4827 (03000f).

    Until the session is ready, those two commands need to be routed back into the negotiation state machine. Our first implementation treated 03000f as normal telemetry, discarded 4822, and the device disconnected before reaching 4040.

This may be specific to the A17C0 / Solarbank 2 AC dynamic path, so we would keep it guarded and separate from the legacy SB2 handshake.

Best regards,
Lumi

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.

5 participants