Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
371 changes: 371 additions & 0 deletions SolixBLE.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,35 @@ class LightStatus(Enum):
#: The light is on high.
HIGH = 3

class OverloadStatus(Enum):
"""The overload status of a port on the device."""

#: Overload status is unknown.
UNKNOWN = -1

#: No overload status.
NO_OVERLOAD = 0

#: Overload for port C1
C1_OVERLOAD = 8

#: Overload for port C2
C2_OVERLOAD = 9

#: Overload for port C3
C3_OVERLOAD = 10

class TemperatureUnit(Enum):
"""The temperature unit."""

#: The status of the temperature unit is unknown.
UNKNOWN = -1

#: The temperature unit is Celsius
CELSIUS = 0

#: The temperature unit is Fahrenheit
FAHRENHEIT = 1

class SolixBLEDevice:
"""Solix BLE device object."""
Expand Down Expand Up @@ -324,6 +353,24 @@ def _parse_int(self, index: int) -> int:
"""
return int.from_bytes(self._data[index : index + 2], byteorder="little")

def _parse_int32(self, index: int) -> int:
"""Parse a 32-bit integer at the index in the telemetry bytes.

:param index: Index of 32-bit integer in array.
:returns: 32-bit integer.
:raises IndexError: If index is out of range.
"""
return int.from_bytes(self._data[index : index + 4], byteorder="little")

def _parse_str(self, index: int, length: int) -> str:
"""Parse a string with length at the index in the telemetry bytes.

:param index: Index of 16-bit integer in array.
:param length: length of string in array.
:raises IndexError: If index or length is out of range.
"""
return self._data[index : index + length].decode("ascii")

def _parse_telemetry(self, data: bytearray) -> None:
"""Update internal values using the telemetry data.

Expand Down Expand Up @@ -870,6 +917,330 @@ def battery_percentage(self) -> int:
return self._data[169] if self._data is not None else DEFAULT_METADATA_INT


class C300DC(SolixBLEDevice):

_EXPECTED_TELEMETRY_LENGTH: int = 252

@property
def car_socket_timeout(self) -> int:
"""Car Socket Timeout.

:returns: Car socket timeout in seconds or default int value.
"""
return self._parse_int32(15) if self._data is not None else DEFAULT_METADATA_INT

@property
def hours_remaining(self) -> float:
"""Time remaining to full/empty.

Note that any hours over 24 are overflowed to the
days remaining. Use time_remaining if you want
days to be included.

:returns: Hours remaining or default float value.
"""
return (
self._data[22] / 10.0 if self._data is not None else DEFAULT_METADATA_FLOAT
)

@property
def days_remaining(self) -> int:
"""Time remaining to full/empty.

:returns: Days remaining or default int value.
"""
return self._data[23] if self._data is not None else DEFAULT_METADATA_INT

@property
def time_remaining(self) -> float:
"""Time remaining to full/empty.

This includes any hours which were overflowed
into days.

:returns: Hours remaining or default float value.
"""
if (
self.hours_remaining == DEFAULT_METADATA_FLOAT
or self.days_remaining == DEFAULT_METADATA_INT
):
return DEFAULT_METADATA_FLOAT

return (self.days_remaining * 24) + self.hours_remaining

@property
def usb_c1_power(self) -> int:
"""USB C1 Power.

:returns: USB port C1 power or default int value.
"""
return self._parse_int(27) if self._data is not None else DEFAULT_METADATA_INT

@property
def usb_c2_power(self) -> int:
"""USB C2 Power.

:returns: USB port C2 power or default int value.
"""
return self._parse_int(32) if self._data is not None else DEFAULT_METADATA_INT

@property
def usb_c3_power(self) -> int:
"""USB C3 Power.

:returns: USB port C3 power or default int value.
"""
return self._parse_int(37) if self._data is not None else DEFAULT_METADATA_INT

@property
def usb_c4_power(self) -> int:
"""USB C4 Power.

:returns: USB port C4 power or default int value.
"""
return self._parse_int(42) if self._data is not None else DEFAULT_METADATA_INT

@property
def usb_a1_power(self) -> int:
"""USB A1 Power.

:returns: USB port A1 power or default int value.
"""
return self._parse_int(47) if self._data is not None else DEFAULT_METADATA_INT

@property
def usb_a2_power(self) -> int:
"""USB A2 Power.

:returns: USB port A2 power or default int value.
"""
return self._parse_int(52) if self._data is not None else DEFAULT_METADATA_INT

@property
def car_socket_power(self) -> int:
"""Car Socket Power.

:returns: Car socket power or default int value.
"""
return self._parse_int(57) if self._data is not None else DEFAULT_METADATA_INT

@property
def solar_power_in(self) -> int:
"""Solar Power In.

:returns: Total solar power in or default int value.
"""
return self._parse_int(62) if self._data is not None else DEFAULT_METADATA_INT

@property
def power_in(self) -> int:
"""Total Power In.

:returns: Total power in or default int value.
"""
return self._parse_int(67) if self._data is not None else DEFAULT_METADATA_INT

@property
def power_out(self) -> int:
"""Total Power Out.

:returns: Total power out or default int value.
"""
return self._parse_int(72) if self._data is not None else DEFAULT_METADATA_INT

@property
def battery_temperature(self) -> int:
"""Battery Temperature Celsius.

:returns: Battery temperature celsius or default int value.
"""
return self._data[112] if self._data is not None else DEFAULT_METADATA_INT

@property
def total_in_out(self) -> PortStatus:
"""Total Power In/Out Status.

:returns: Status of the total power in/out.
"""
return PortStatus(
self._data[116] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def battery_percentage(self) -> int:
"""Battery Percentage.

:returns: Percentage charge of battery or default int value.
"""
return self._data[120] if self._data is not None else DEFAULT_METADATA_INT

@property
def usb_port_c1(self) -> PortStatus:
"""USB C1 Port Status.

:returns: Status of the USB C1 port.
"""
return PortStatus(
self._data[128] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def usb_port_c2(self) -> PortStatus:
"""USB C2 Port Status.

:returns: Status of the USB C2 port.
"""
return PortStatus(
self._data[132] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def usb_port_c3(self) -> PortStatus:
"""USB C3 Port Status.

:returns: Status of the USB C3 port.
"""
return PortStatus(
self._data[136] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def usb_port_c4(self) -> PortStatus:
"""USB C4 Port Status.

:returns: Status of the USB C4 port.
"""
return PortStatus(
self._data[140] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def usb_port_a1(self) -> PortStatus:
"""USB A1 Port Status.

:returns: Status of the USB A1 port.
"""
return PortStatus(
self._data[144] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def usb_port_a2(self) -> PortStatus:
"""USB A2 Port Status.

:returns: Status of the USB A2 port.
"""
return PortStatus(
self._data[148] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def car_socket_port(self) -> PortStatus:
"""Car Socket Port Status.

:returns: Status of the Car socket port.
"""
return PortStatus(
self._data[152] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def port_overload(self) -> PortStatus:
"""Port Overload Status.

:returns: Overload Status of a port.
"""
return OverloadStatus(
self._data[160] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def serial_number(self) -> str:
"""Serial Number.

:returns: Serial number as string or default string value.
"""
return self._parse_str(168, 16) if self._data is not None else DEFAULT_METADATA_STRING

@property
def device_standby(self) -> int:
"""Device Standby Setting.

:returns: Configured device standby in minutes or default int value.
"""
return self._parse_int(187) if self._data is not None else DEFAULT_METADATA_INT

@property
def display_timeout(self) -> int:
"""Display Timeout Setting.

:returns: Configured display timeout in seconds or default int value.
"""
return self._parse_int(192) if self._data is not None else DEFAULT_METADATA_INT

@property
def display_brightness(self) -> int:
"""Display Brightness Setting.

:returns: Configured display brightness or default int value.
"""
return self._data[202] if self._data is not None else DEFAULT_METADATA_INT

@property
def light_status(self) -> int:
"""Light Status.

:returns: Status of the light or default int value.
"""
return LightStatus(
self._data[206] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def temperature_unit(self) -> int:
"""Temperature Unit Setting.

:returns: Configured temperature unit or default int value.
"""
return TemperatureUnit(
self._data[210] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def display_status(self) -> int:
"""Display Status.

:returns: Status of the display or default int value.
"""
return self._data[214] if self._data is not None else DEFAULT_METADATA_INT

@property
def light_timeout(self) -> int:
"""Light Timeout Setting.

:returns: Configured light timeout in minutes or default int value.
"""
return self._parse_int(218) if self._data is not None else DEFAULT_METADATA_INT

@property
def solar_in_port(self) -> PortStatus:
"""Solar In Port Status.

:returns: Status of the Solar In port.
"""
return PortStatus(
self._data[227] if self._data is not None else DEFAULT_METADATA_INT
)

@property
def car_socket_auto_standby_mode(self) -> int:
"""Car socket auto standby mode.

:returns: Car socket auto standby mode or default int value.
"""
return self._data[231] if self._data is not None else DEFAULT_METADATA_INT


class Generic(SolixBLEDevice):
"""
Generic to be used for adding support for an unsupported device.
Expand Down