sok-ble 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,8 @@ from __future__ import annotations
4
4
 
5
5
  from contextlib import asynccontextmanager
6
6
  from typing import AsyncIterator, Optional
7
+ import asyncio
8
+ import struct
7
9
  import logging
8
10
  import statistics
9
11
 
@@ -65,32 +67,55 @@ class SokBluetoothDevice:
65
67
  await client.disconnect()
66
68
  logger.debug("Disconnected from %s", self._ble_device.address)
67
69
 
70
+ async def _send_command(
71
+ self, client: BleakClientWithServiceCache, cmd: int, expected: int
72
+ ) -> bytes:
73
+ """Send a command and return the response bytes with the given header."""
74
+
75
+ # If the client supports notifications (real BLE client), prefer that
76
+ start_notify = getattr(client, "start_notify", None)
77
+ if start_notify is None:
78
+ await client.write_gatt_char(UUID_TX, _sok_command(cmd))
79
+ data = bytes(await client.read_gatt_char(UUID_RX))
80
+ return data
81
+
82
+ queue: asyncio.Queue[bytes] = asyncio.Queue()
83
+
84
+ def handler(_: int, data: bytearray) -> None:
85
+ queue.put_nowait(bytes(data))
86
+
87
+ await client.start_notify(UUID_RX, handler)
88
+ try:
89
+ await client.write_gatt_char(UUID_TX, _sok_command(cmd))
90
+ while True:
91
+ data = await asyncio.wait_for(queue.get(), 5.0)
92
+ if struct.unpack_from(">H", data)[0] == expected:
93
+ return data
94
+ finally:
95
+ await client.stop_notify(UUID_RX)
96
+
68
97
  async def async_update(self) -> None:
69
98
  """Poll the device for all telemetry and update attributes."""
70
99
  responses: dict[int, bytes] = {}
71
100
  async with self._connect() as client:
72
101
  logger.debug("Send C1")
73
- await client.write_gatt_char(UUID_TX, _sok_command(0xC1))
74
- data = bytes(await client.read_gatt_char(UUID_RX))
75
- logger.debug("Recv 0xCCF0: %s", data.hex())
102
+ data = await self._send_command(client, 0xC1, 0xCCF0)
103
+ logger.debug("Recv 0x%04X: %s", struct.unpack_from(">H", data)[0], data.hex())
76
104
  responses[0xCCF0] = data
77
105
 
78
106
  logger.debug("Send C1")
79
- await client.write_gatt_char(UUID_TX, _sok_command(0xC1))
80
- data = bytes(await client.read_gatt_char(UUID_RX))
81
- logger.debug("Recv 0xCCF2: %s", data.hex())
107
+ data = await self._send_command(client, 0xC1, 0xCCF2)
108
+ logger.debug("Recv 0x%04X: %s", struct.unpack_from(">H", data)[0], data.hex())
82
109
  responses[0xCCF2] = data
83
110
 
84
111
  logger.debug("Send C2")
85
- await client.write_gatt_char(UUID_TX, _sok_command(0xC2))
86
- data = bytes(await client.read_gatt_char(UUID_RX))
87
- logger.debug("Recv 0xCCF3: %s", data.hex())
112
+ data = await self._send_command(client, 0xC2, 0xCCF3)
113
+ logger.debug("Recv 0x%04X: %s", struct.unpack_from(">H", data)[0], data.hex())
88
114
  responses[0xCCF3] = data
89
115
 
90
116
  logger.debug("Send C2")
91
- await client.write_gatt_char(UUID_TX, _sok_command(0xC2))
92
- data = bytes(await client.read_gatt_char(UUID_RX))
93
- logger.debug("Recv 0xCCF4: %s", data.hex())
117
+ data = await self._send_command(client, 0xC2, 0xCCF4)
118
+ logger.debug("Recv 0x%04X: %s", struct.unpack_from(">H", data)[0], data.hex())
94
119
  responses[0xCCF4] = data
95
120
 
96
121
  parsed = SokParser.parse_all(responses)
sok_ble/sok_parser.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import logging
6
6
  import struct
7
+ import statistics
7
8
  from typing import Dict, Sequence
8
9
 
9
10
  from sok_ble.exceptions import InvalidResponseError
@@ -43,27 +44,19 @@ class SokParser:
43
44
 
44
45
  @staticmethod
45
46
  def parse_info(buf: bytes) -> Dict[str, float | int]:
46
- """Parse the information frame for voltage, current and SOC."""
47
+ """Parse the information frame for current, SOC and cycles."""
47
48
  logger.debug("parse_info input: %s", buf.hex())
48
- if len(buf) < 18:
49
+ if len(buf) < 20:
49
50
  raise InvalidResponseError("Info buffer too short")
50
51
 
51
- cells = [
52
- get_le_ushort(buf, 0),
53
- get_le_ushort(buf, 2),
54
- get_le_ushort(buf, 4),
55
- get_le_ushort(buf, 6),
56
- ]
57
- voltage = (sum(cells) / len(cells) * 4) / 1000
58
-
59
- current = get_le_int3(buf, 8) / 10
60
-
61
- soc = struct.unpack_from('<H', buf, 16)[0]
52
+ current = get_le_int3(buf, 5) / 1000
53
+ num_cycles = get_le_ushort(buf, 14)
54
+ soc = get_le_ushort(buf, 16)
62
55
 
63
56
  result = {
64
- "voltage": voltage,
65
57
  "current": current,
66
58
  "soc": soc,
59
+ "num_cycles": num_cycles,
67
60
  }
68
61
  logger.debug("parse_info result: %s", result)
69
62
  return result
@@ -72,27 +65,23 @@ class SokParser:
72
65
  def parse_temps(buf: bytes) -> float:
73
66
  """Parse the temperature from the temperature frame."""
74
67
  logger.debug("parse_temps input: %s", buf.hex())
75
- if len(buf) < 7:
68
+ if len(buf) < 20:
76
69
  raise InvalidResponseError("Temp buffer too short")
77
70
 
78
- temperature = get_le_short(buf, 5) / 10
71
+ temperature = get_le_short(buf, 5)
79
72
  logger.debug("parse_temps result: %s", temperature)
80
73
  return temperature
81
74
 
82
75
  @staticmethod
83
76
  def parse_capacity_cycles(buf: bytes) -> Dict[str, float | int]:
84
- """Parse rated capacity and cycle count."""
77
+ """Parse rated capacity."""
85
78
  logger.debug("parse_capacity_cycles input: %s", buf.hex())
86
- if len(buf) < 6:
79
+ if len(buf) < 20:
87
80
  raise InvalidResponseError("Capacity buffer too short")
88
81
 
89
- capacity = get_le_ushort(buf, 0) / 100
90
- num_cycles = get_le_ushort(buf, 4)
82
+ capacity = get_be_uint3(buf, 5) / 128
91
83
 
92
- result = {
93
- "capacity": capacity,
94
- "num_cycles": num_cycles,
95
- }
84
+ result = {"capacity": capacity}
96
85
  logger.debug("parse_capacity_cycles result: %s", result)
97
86
  return result
98
87
 
@@ -100,15 +89,13 @@ class SokParser:
100
89
  def parse_cells(buf: bytes) -> list[float]:
101
90
  """Parse individual cell voltages."""
102
91
  logger.debug("parse_cells input: %s", buf.hex())
103
- if len(buf) < 8:
92
+ if len(buf) < 20:
104
93
  raise InvalidResponseError("Cells buffer too short")
105
94
 
106
- cells = [
107
- get_le_ushort(buf, 0) / 1000,
108
- get_le_ushort(buf, 2) / 1000,
109
- get_le_ushort(buf, 4) / 1000,
110
- get_le_ushort(buf, 6) / 1000,
111
- ]
95
+ cells = [0.0, 0.0, 0.0, 0.0]
96
+ for x in range(4):
97
+ cell_idx = buf[2 + x * 4]
98
+ cells[cell_idx - 1] = get_le_ushort(buf, 3 + x * 4) / 1000
112
99
  logger.debug("parse_cells result: %s", cells)
113
100
  return cells
114
101
 
@@ -125,10 +112,15 @@ class SokParser:
125
112
  capacity_info = cls.parse_capacity_cycles(responses[0xCCF3])
126
113
  cells = cls.parse_cells(responses[0xCCF4])
127
114
 
115
+ voltage = statistics.mean(cells) * 4
116
+
128
117
  result = {
129
- **info,
118
+ "voltage": voltage,
119
+ "current": info["current"],
120
+ "soc": info["soc"],
130
121
  "temperature": temperature,
131
- **capacity_info,
122
+ "capacity": capacity_info["capacity"],
123
+ "num_cycles": info["num_cycles"],
132
124
  "cell_voltages": cells,
133
125
  }
134
126
  logger.debug("parse_all result: %s", result)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sok-ble
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: SOK BLE battery interface library
5
5
  Author-email: Mitchell Carlson <mitchell.carlson.pro@gmail.com>
6
6
  License: Apache-2.0
@@ -0,0 +1,9 @@
1
+ sok_ble/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sok_ble/const.py,sha256=mHtJTbWz_dG3v1lhZrLDzMf-QAG9v5QWlHU9ZKsyYdg,998
3
+ sok_ble/exceptions.py,sha256=7H1yUqqnrKyi3LwxRCMF7RkuGp_rgdy9j35nrMOgz44,247
4
+ sok_ble/sok_bluetooth_device.py,sha256=ig11wLwjZFC6HEFi7hzB7_Jy1T8syyNDOTYWUbkJR4Y,6353
5
+ sok_ble/sok_parser.py,sha256=KXYxsR4868vWFs2AAI7o7XfzjNZiDgfpsmz3dFgjLco,4323
6
+ sok_ble-0.1.1.dist-info/METADATA,sha256=Hub0gJn6lH2WJVXpSFaiYQ2vKv4nVdDvypZyNITBN2g,1233
7
+ sok_ble-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ sok_ble-0.1.1.dist-info/top_level.txt,sha256=WTVtlZ2sADYAxKEBkDJPUn4hFAH9NfIZ9Q2HP2RKpbw,8
9
+ sok_ble-0.1.1.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- sok_ble/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- sok_ble/const.py,sha256=mHtJTbWz_dG3v1lhZrLDzMf-QAG9v5QWlHU9ZKsyYdg,998
3
- sok_ble/exceptions.py,sha256=7H1yUqqnrKyi3LwxRCMF7RkuGp_rgdy9j35nrMOgz44,247
4
- sok_ble/sok_bluetooth_device.py,sha256=-dMuHzM0FtBh_fu2MURiriOVhOGaLDM2ZQIRk6zDC84,5384
5
- sok_ble/sok_parser.py,sha256=zyrZwjOc4yeFfQWo_1B3VFxEGXLJBX7UQPL9e5ag1jQ,4441
6
- sok_ble-0.1.0.dist-info/METADATA,sha256=u1fvysxad3Cg-N2ipnnuhB2V4N8iVQEzel4-INBRpnU,1233
7
- sok_ble-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- sok_ble-0.1.0.dist-info/top_level.txt,sha256=WTVtlZ2sADYAxKEBkDJPUn4hFAH9NfIZ9Q2HP2RKpbw,8
9
- sok_ble-0.1.0.dist-info/RECORD,,