python-ember-mug 0.7.0b4__tar.gz → 0.7.0b6__tar.gz
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.
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/PKG-INFO +1 -1
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/__init__.py +1 -1
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/cli/commands.py +18 -4
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/consts.py +1 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/data.py +8 -2
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/mug.py +21 -11
- python_ember_mug-0.7.0b6/ember_mug/utils.py +100 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/pyproject.toml +1 -1
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/test_consts.py +11 -1
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/test_mug_data.py +1 -0
- python_ember_mug-0.7.0b6/tests/test_utils.py +70 -0
- python_ember_mug-0.7.0b4/ember_mug/utils.py +0 -62
- python_ember_mug-0.7.0b4/tests/test_utils.py +0 -32
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/LICENSE +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/README.md +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/__main__.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/cli/__init__.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/cli/helpers.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/formatting.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/ember_mug/scanner.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/__init__.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/cli/__init__.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/cli/test_commands.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/cli/test_helpers.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/conftest.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/test_connection.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/test_data.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/test_formatting.py +0 -0
- {python_ember_mug-0.7.0b4 → python_ember_mug-0.7.0b6}/tests/test_scanner.py +0 -0
|
@@ -12,7 +12,7 @@ from typing import TYPE_CHECKING
|
|
|
12
12
|
|
|
13
13
|
from bleak import BleakError
|
|
14
14
|
|
|
15
|
-
from ..consts import ATTR_LABELS, EXTRA_ATTRS
|
|
15
|
+
from ..consts import ATTR_LABELS, EXTRA_ATTRS, VolumeLevel
|
|
16
16
|
from ..data import Colour
|
|
17
17
|
from ..mug import EmberMug
|
|
18
18
|
from ..scanner import discover_mugs, find_mug
|
|
@@ -103,7 +103,11 @@ async def get_mug_value(args: Namespace) -> None:
|
|
|
103
103
|
attributes = [a.replace('-', '_') for a in args.attributes]
|
|
104
104
|
async with mug.connection(adapter=args.adapter):
|
|
105
105
|
for attr in attributes:
|
|
106
|
-
|
|
106
|
+
try:
|
|
107
|
+
value = await getattr(mug, f'get_{attr}')()
|
|
108
|
+
except NotImplementedError as e:
|
|
109
|
+
print(e)
|
|
110
|
+
sys.exit(1)
|
|
107
111
|
setattr(mug.data, attr, value)
|
|
108
112
|
data[attr] = value
|
|
109
113
|
if args.raw:
|
|
@@ -114,7 +118,7 @@ async def get_mug_value(args: Namespace) -> None:
|
|
|
114
118
|
|
|
115
119
|
async def set_mug_value(args: Namespace) -> None:
|
|
116
120
|
"""Set one or more values on the mug."""
|
|
117
|
-
attrs = ('name', 'target_temp', 'temperature_unit', 'led_colour')
|
|
121
|
+
attrs = ('name', 'target_temp', 'temperature_unit', 'led_colour', 'volume_level')
|
|
118
122
|
values = [(attr, value) for attr in attrs if (value := getattr(args, attr))]
|
|
119
123
|
if not values:
|
|
120
124
|
print('Please specify at least one attribute and value to set.')
|
|
@@ -127,7 +131,11 @@ async def set_mug_value(args: Namespace) -> None:
|
|
|
127
131
|
for attr, value in values:
|
|
128
132
|
method = getattr(mug, f'set_{attr.replace("-", "_")}')
|
|
129
133
|
print(f'Setting {attr} to {value}')
|
|
130
|
-
|
|
134
|
+
try:
|
|
135
|
+
await method(value)
|
|
136
|
+
except NotImplementedError as e:
|
|
137
|
+
print(e)
|
|
138
|
+
sys.exit(1)
|
|
131
139
|
|
|
132
140
|
|
|
133
141
|
def colour_type(value: str) -> Colour:
|
|
@@ -205,6 +213,12 @@ class EmberMugCli:
|
|
|
205
213
|
set_parser.add_argument('--target-temp', help='Target Temperature', type=float, required=False)
|
|
206
214
|
set_parser.add_argument('--temperature-unit', help='Temperature Unit', choices=['C', 'F'], required=False)
|
|
207
215
|
set_parser.add_argument('--led-colour', help='LED Colour', type=colour_type, required=False)
|
|
216
|
+
set_parser.add_argument(
|
|
217
|
+
'--volume-level',
|
|
218
|
+
help='Volume Level',
|
|
219
|
+
choices=[v.value for v in VolumeLevel],
|
|
220
|
+
required=False,
|
|
221
|
+
)
|
|
208
222
|
|
|
209
223
|
async def run(self) -> None:
|
|
210
224
|
"""Run the specified command based on subparser."""
|
|
@@ -205,6 +205,7 @@ EXTRA_ATTRS = {'dsk', 'udsk', 'battery_voltage', 'date_time_zone'}
|
|
|
205
205
|
|
|
206
206
|
# Validation
|
|
207
207
|
MUG_NAME_REGEX = re.compile(r"^[A-Za-z0-9,.\[\]#()!\"\';:|\-_+<>%= ]{1,16}$")
|
|
208
|
+
MUG_NAME_PATTERN = MUG_NAME_REGEX.pattern
|
|
208
209
|
MAC_ADDRESS_REGEX = re.compile(r"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$")
|
|
209
210
|
|
|
210
211
|
IS_LINUX = platform.system() == "Linux"
|
|
@@ -155,6 +155,11 @@ class Model:
|
|
|
155
155
|
return INITIAL_ATTRS - EXTRA_ATTRS
|
|
156
156
|
return INITIAL_ATTRS
|
|
157
157
|
|
|
158
|
+
@cached_property
|
|
159
|
+
def all_attributes(self) -> set[str]:
|
|
160
|
+
"""All attributes."""
|
|
161
|
+
return self.initial_attributes | self.update_attributes
|
|
162
|
+
|
|
158
163
|
@cached_property
|
|
159
164
|
def update_attributes(self) -> set[str]:
|
|
160
165
|
"""Attributes to update based on model and extra."""
|
|
@@ -279,8 +284,9 @@ class MugData:
|
|
|
279
284
|
data = {k: asdict(v) if is_dataclass(v) else v for k, v in asdict(self).items()}
|
|
280
285
|
data.update(
|
|
281
286
|
{
|
|
282
|
-
f'{attr}_display': getattr(self, f'{attr}_display')
|
|
283
|
-
for attr in
|
|
287
|
+
f'{attr}_display': getattr(self, f'{attr}_display', None)
|
|
288
|
+
for attr in self.model.all_attributes
|
|
289
|
+
if hasattr(self, f'{attr}_display')
|
|
284
290
|
},
|
|
285
291
|
)
|
|
286
292
|
return data
|
|
@@ -31,8 +31,8 @@ from .utils import (
|
|
|
31
31
|
bytes_to_big_int,
|
|
32
32
|
bytes_to_little_int,
|
|
33
33
|
decode_byte_string,
|
|
34
|
+
discover_services,
|
|
34
35
|
encode_byte_string,
|
|
35
|
-
log_services,
|
|
36
36
|
temp_from_bytes,
|
|
37
37
|
)
|
|
38
38
|
|
|
@@ -69,6 +69,10 @@ class EmberMug:
|
|
|
69
69
|
self._latest_events: dict[int, float] = {}
|
|
70
70
|
self._client_kwargs: dict[str, str] = {}
|
|
71
71
|
|
|
72
|
+
# Just shortcuts, the value doesn't change once initialized
|
|
73
|
+
self.is_travel_mug = self.data.model.is_travel_mug
|
|
74
|
+
self.is_cup = self.data.model.is_cup
|
|
75
|
+
|
|
72
76
|
logger.debug("New mug connection initialized.")
|
|
73
77
|
self.set_client_options(**kwargs)
|
|
74
78
|
|
|
@@ -105,7 +109,7 @@ class EmberMug:
|
|
|
105
109
|
ble_device_callback=lambda: self.device,
|
|
106
110
|
)
|
|
107
111
|
if self.debug is True:
|
|
108
|
-
|
|
112
|
+
await discover_services(client)
|
|
109
113
|
self._expected_disconnect = False
|
|
110
114
|
except (asyncio.TimeoutError, BleakError) as error:
|
|
111
115
|
logger.error("%s: Failed to connect to the mug: %s", self.device, error)
|
|
@@ -195,10 +199,14 @@ class EmberMug:
|
|
|
195
199
|
|
|
196
200
|
async def get_led_colour(self) -> Colour:
|
|
197
201
|
"""Get RGBA colours from mug gatt."""
|
|
202
|
+
if self.is_travel_mug is True:
|
|
203
|
+
raise NotImplementedError('The Travel Mug does not have an LED colour attribute')
|
|
198
204
|
return Colour.from_bytes(await self._read(MugCharacteristic.LED))
|
|
199
205
|
|
|
200
206
|
async def set_led_colour(self, colour: Colour) -> None:
|
|
201
207
|
"""Set new target temp for mug."""
|
|
208
|
+
if self.is_travel_mug is True:
|
|
209
|
+
raise NotImplementedError('The Travel Mug does not have an LED colour attribute')
|
|
202
210
|
colour = Colour(*colour[:3], 255) # It always expects 255 for alpha
|
|
203
211
|
await self._write(MugCharacteristic.LED, colour.as_bytearray())
|
|
204
212
|
self.data.led_colour = colour
|
|
@@ -226,13 +234,9 @@ class EmberMug:
|
|
|
226
234
|
|
|
227
235
|
async def get_volume_level(self) -> VolumeLevel | None:
|
|
228
236
|
"""Get volume level from mug gatt."""
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if not self.data.model.is_travel_mug:
|
|
233
|
-
raise NotImplementedError('Ony the travel mug has a volume')
|
|
234
|
-
logger.error('Failed to fetch volume attribute: %s', e)
|
|
235
|
-
return None
|
|
237
|
+
if self.is_travel_mug is False:
|
|
238
|
+
raise NotImplementedError('The Mug and Cup do not have a volume level attribute')
|
|
239
|
+
volume_bytes = await self._read(MugCharacteristic.VOLUME)
|
|
236
240
|
volume_int = bytes_to_little_int(volume_bytes)
|
|
237
241
|
return VolumeLevel.from_state(volume_int)
|
|
238
242
|
|
|
@@ -240,8 +244,10 @@ class EmberMug:
|
|
|
240
244
|
"""Set volume_level on Travel Mug."""
|
|
241
245
|
if volume not in (0, 1, 2):
|
|
242
246
|
raise ValueError('Volume level must be between 0 and 2 inclusively')
|
|
247
|
+
if self.is_travel_mug is False:
|
|
248
|
+
raise NotImplementedError('The Mug and Cup do not have a volume level attribute')
|
|
243
249
|
volume_level = volume if isinstance(volume, VolumeLevel) else VolumeLevel.from_state(volume)
|
|
244
|
-
await self._write(MugCharacteristic.VOLUME, bytearray(
|
|
250
|
+
await self._write(MugCharacteristic.VOLUME, bytearray([volume_level.state]))
|
|
245
251
|
self.data.volume_level = volume_level
|
|
246
252
|
|
|
247
253
|
async def get_liquid_state(self) -> LiquidState:
|
|
@@ -252,13 +258,17 @@ class EmberMug:
|
|
|
252
258
|
|
|
253
259
|
async def get_name(self) -> str:
|
|
254
260
|
"""Get mug name from gatt."""
|
|
261
|
+
if self.is_cup is True:
|
|
262
|
+
raise NotImplementedError('The Cup does not have a name attribute')
|
|
255
263
|
name_bytes: bytearray = await self._read(MugCharacteristic.MUG_NAME)
|
|
256
264
|
return bytes(name_bytes).decode("utf8")
|
|
257
265
|
|
|
258
266
|
async def set_name(self, name: str) -> None:
|
|
259
267
|
"""Assign new name to mug."""
|
|
260
268
|
if MUG_NAME_REGEX.match(name) is None:
|
|
261
|
-
raise ValueError('Name cannot contain any special characters')
|
|
269
|
+
raise ValueError('Name cannot contain any special characters and must be 16 characters or less')
|
|
270
|
+
if self.is_cup is True:
|
|
271
|
+
raise NotImplementedError('The Cup does not have a name attribute')
|
|
262
272
|
await self._write(MugCharacteristic.MUG_NAME, bytearray(name.encode("utf8")))
|
|
263
273
|
self.data.name = name
|
|
264
274
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Helpful utils for processing mug data."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import contextlib
|
|
6
|
+
import logging
|
|
7
|
+
import re
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from bleak import BleakError
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from bleak import BleakClient
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def decode_byte_string(data: bytes | bytearray) -> str:
|
|
19
|
+
"""Convert bytes to text as Ember expects."""
|
|
20
|
+
if not data:
|
|
21
|
+
return ''
|
|
22
|
+
with contextlib.suppress(ValueError):
|
|
23
|
+
b64_as_str = base64.encodebytes(data).decode("utf-8")
|
|
24
|
+
return re.sub("[\r\n]", "", b64_as_str)
|
|
25
|
+
logger.warning('Failed to decode bytes "%s". Forcing to string.', data)
|
|
26
|
+
return str(data)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def encode_byte_string(data: str) -> bytes:
|
|
30
|
+
"""Encode string from Ember Mug."""
|
|
31
|
+
return re.sub(b"[\r\n]", b"", base64.encodebytes(data.encode("utf8")))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def bytes_to_little_int(data: bytearray | bytes) -> int:
|
|
35
|
+
"""Convert bytes to little int."""
|
|
36
|
+
return int.from_bytes(data, byteorder="little", signed=False)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def bytes_to_big_int(data: bytearray | bytes) -> int:
|
|
40
|
+
"""Convert bytes to big int."""
|
|
41
|
+
return int.from_bytes(data, byteorder="big")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def temp_from_bytes(temp_bytes: bytearray, metric: bool = True) -> float:
|
|
45
|
+
"""Get temperature from bytearray and convert to Fahrenheit if needed."""
|
|
46
|
+
temp = float(bytes_to_little_int(temp_bytes)) * 0.01
|
|
47
|
+
if metric is False:
|
|
48
|
+
# Convert to fahrenheit
|
|
49
|
+
temp = (temp * 9 / 5) + 32
|
|
50
|
+
return round(temp, 2)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def discover_services(client: BleakClient) -> dict[str, Any]:
|
|
54
|
+
"""Log all services and all values for debugging/development."""
|
|
55
|
+
logger.info("Logging all services that were discovered")
|
|
56
|
+
services: dict[str, Any] = {}
|
|
57
|
+
for service in client.services:
|
|
58
|
+
logger.debug("[Service] %s: %s", service.uuid, service.description)
|
|
59
|
+
characteristics: dict[str, Any] = {}
|
|
60
|
+
services[service.uuid] = {
|
|
61
|
+
'uuid': service.uuid,
|
|
62
|
+
'characteristics': characteristics,
|
|
63
|
+
}
|
|
64
|
+
for characteristic in service.characteristics:
|
|
65
|
+
value: bytes | BleakError | None = None
|
|
66
|
+
if "read" in characteristic.properties:
|
|
67
|
+
try:
|
|
68
|
+
value = bytes(await client.read_gatt_char(characteristic.uuid))
|
|
69
|
+
except BleakError as e:
|
|
70
|
+
value = e
|
|
71
|
+
logger.debug(
|
|
72
|
+
"\t[Characteristic] %s: %s | Description: %s | Value: '%s'",
|
|
73
|
+
characteristic.uuid,
|
|
74
|
+
",".join(characteristic.properties),
|
|
75
|
+
characteristic.description,
|
|
76
|
+
value,
|
|
77
|
+
)
|
|
78
|
+
descriptors: list[dict[str, Any]] = []
|
|
79
|
+
characteristics[characteristic.uuid] = {
|
|
80
|
+
'uuid': characteristic.uuid,
|
|
81
|
+
'properties': characteristic.properties,
|
|
82
|
+
'value': value,
|
|
83
|
+
'descriptors': descriptors,
|
|
84
|
+
}
|
|
85
|
+
for descriptor in characteristic.descriptors:
|
|
86
|
+
value = bytes(await client.read_gatt_descriptor(descriptor.handle))
|
|
87
|
+
logger.debug(
|
|
88
|
+
"\t\t[Descriptor] %s: Handle: %s | Value: '%s'",
|
|
89
|
+
descriptor.uuid,
|
|
90
|
+
descriptor.handle,
|
|
91
|
+
value,
|
|
92
|
+
)
|
|
93
|
+
descriptors.append(
|
|
94
|
+
{
|
|
95
|
+
'uuid': descriptor.uuid,
|
|
96
|
+
'handle': descriptor.handle,
|
|
97
|
+
'value': value,
|
|
98
|
+
},
|
|
99
|
+
)
|
|
100
|
+
return services
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
|
|
6
|
-
from ember_mug.consts import LiquidState, MugCharacteristic
|
|
6
|
+
from ember_mug.consts import LiquidState, MugCharacteristic, VolumeLevel
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def test_mug_uuids() -> None:
|
|
@@ -20,3 +20,13 @@ def test_liquid_state() -> None:
|
|
|
20
20
|
assert LiquidState(5).label == "Heating"
|
|
21
21
|
assert LiquidState(6).label == "Perfect"
|
|
22
22
|
assert LiquidState(7).label == "Warm (No control)"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_volume_level() -> None:
|
|
26
|
+
assert VolumeLevel.from_state(0) == VolumeLevel.LOW
|
|
27
|
+
assert VolumeLevel.from_state(1) == VolumeLevel.MEDIUM
|
|
28
|
+
assert VolumeLevel.from_state(2) == VolumeLevel.HIGH
|
|
29
|
+
|
|
30
|
+
assert VolumeLevel.HIGH.state == 2
|
|
31
|
+
assert VolumeLevel.MEDIUM.state == 1
|
|
32
|
+
assert VolumeLevel.LOW.state == 0
|
|
@@ -92,6 +92,7 @@ def test_mug_dict(mug_data: MugData) -> None:
|
|
|
92
92
|
'liquid_state': LiquidState.UNKNOWN,
|
|
93
93
|
'liquid_state_display': 'Unknown',
|
|
94
94
|
'meta': {'mug_id': 'test_id', 'serial_number': 'serial number'},
|
|
95
|
+
'meta_display': 'Serial Number: serial number',
|
|
95
96
|
'name': '',
|
|
96
97
|
'target_temp': 0.0,
|
|
97
98
|
'target_temp_display': '0.00°C',
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Tests for `ember_mug.utils`."""
|
|
2
|
+
from unittest.mock import AsyncMock, MagicMock, Mock, call, patch
|
|
3
|
+
|
|
4
|
+
from ember_mug.utils import (
|
|
5
|
+
bytes_to_big_int,
|
|
6
|
+
bytes_to_little_int,
|
|
7
|
+
decode_byte_string,
|
|
8
|
+
discover_services,
|
|
9
|
+
encode_byte_string,
|
|
10
|
+
temp_from_bytes,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_bytes_to_little_int() -> None:
|
|
15
|
+
assert bytes_to_little_int(b'\x05') == 5
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_bytes_to_big_int() -> None:
|
|
19
|
+
assert bytes_to_big_int(b'\x01\xc2') == 450
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_temp_from_bytes() -> None:
|
|
23
|
+
raw_data = bytearray(b'\xcd\x15') # int: 5581
|
|
24
|
+
assert temp_from_bytes(raw_data) == 55.81
|
|
25
|
+
assert temp_from_bytes(raw_data, metric=False) == 132.46
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_decode_byte_string() -> None:
|
|
29
|
+
assert decode_byte_string(b'abcd12345') == 'YWJjZDEyMzQ1'
|
|
30
|
+
assert decode_byte_string(b'') == ''
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_encode_byte_string() -> None:
|
|
34
|
+
assert encode_byte_string('abcd12345') == b'YWJjZDEyMzQ1'
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@patch('ember_mug.utils.logger')
|
|
38
|
+
async def test_discover_services(read_gatt_descriptor: Mock) -> None:
|
|
39
|
+
mock_descriptor = MagicMock(uuid='test-desc', handle=2)
|
|
40
|
+
mock_characteristic = MagicMock(
|
|
41
|
+
uuid='char-abc',
|
|
42
|
+
description='test char',
|
|
43
|
+
properties=['read'],
|
|
44
|
+
descriptors=[mock_descriptor],
|
|
45
|
+
)
|
|
46
|
+
mock_service = MagicMock(
|
|
47
|
+
uuid='service-abc',
|
|
48
|
+
description='test service',
|
|
49
|
+
characteristics=[mock_characteristic],
|
|
50
|
+
)
|
|
51
|
+
client = AsyncMock(services=[mock_service])
|
|
52
|
+
client.read_gatt_char = AsyncMock(return_value=bytearray(b'test char'))
|
|
53
|
+
client.read_gatt_descriptor = AsyncMock(return_value=bytearray(b'test descriptor'))
|
|
54
|
+
await discover_services(client)
|
|
55
|
+
read_gatt_descriptor.assert_has_calls(
|
|
56
|
+
[
|
|
57
|
+
call.info('Logging all services that were discovered'),
|
|
58
|
+
call.debug('[Service] %s: %s', 'service-abc', 'test service'),
|
|
59
|
+
call.debug(
|
|
60
|
+
"\t[Characteristic] %s: %s | Description: %s | Value: '%s'",
|
|
61
|
+
'char-abc',
|
|
62
|
+
'read',
|
|
63
|
+
'test char',
|
|
64
|
+
b'test char',
|
|
65
|
+
),
|
|
66
|
+
call.debug("\t\t[Descriptor] %s: Handle: %s | Value: '%s'", 'test-desc', 2, b'test descriptor'),
|
|
67
|
+
],
|
|
68
|
+
)
|
|
69
|
+
client.read_gatt_char.assert_called_once_with('char-abc')
|
|
70
|
+
client.read_gatt_descriptor.assert_called_once_with(2)
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"""Helpful utils for processing mug data."""
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import base64
|
|
5
|
-
import contextlib
|
|
6
|
-
import logging
|
|
7
|
-
import re
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
|
-
|
|
10
|
-
if TYPE_CHECKING:
|
|
11
|
-
from bleak import BleakGATTServiceCollection
|
|
12
|
-
|
|
13
|
-
logger = logging.getLogger(__name__)
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def decode_byte_string(data: bytes | bytearray) -> str:
|
|
17
|
-
"""Convert bytes to text as Ember expects."""
|
|
18
|
-
if not data:
|
|
19
|
-
return ''
|
|
20
|
-
with contextlib.suppress(ValueError):
|
|
21
|
-
b64_as_str = base64.encodebytes(data).decode("utf-8")
|
|
22
|
-
return re.sub("[\r\n]", "", b64_as_str)
|
|
23
|
-
logger.warning('Failed to decode bytes "%s". Forcing to string.', data)
|
|
24
|
-
return str(data)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def encode_byte_string(data: str) -> bytes:
|
|
28
|
-
"""Encode string from Ember Mug."""
|
|
29
|
-
return re.sub(b"[\r\n]", b"", base64.encodebytes(data.encode("utf8")))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def bytes_to_little_int(data: bytearray | bytes) -> int:
|
|
33
|
-
"""Convert bytes to little int."""
|
|
34
|
-
return int.from_bytes(data, byteorder="little", signed=False)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def bytes_to_big_int(data: bytearray | bytes) -> int:
|
|
38
|
-
"""Convert bytes to big int."""
|
|
39
|
-
return int.from_bytes(data, byteorder="big")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def temp_from_bytes(temp_bytes: bytearray, metric: bool = True) -> float:
|
|
43
|
-
"""Get temperature from bytearray and convert to Fahrenheit if needed."""
|
|
44
|
-
temp = float(bytes_to_little_int(temp_bytes)) * 0.01
|
|
45
|
-
if metric is False:
|
|
46
|
-
# Convert to fahrenheit
|
|
47
|
-
temp = (temp * 9 / 5) + 32
|
|
48
|
-
return round(temp, 2)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def log_services(services: BleakGATTServiceCollection) -> None:
|
|
52
|
-
"""Log services for debugging."""
|
|
53
|
-
logger.debug("Logging all services that were discovered")
|
|
54
|
-
for service in services:
|
|
55
|
-
logger.debug(
|
|
56
|
-
"Service '%s' (UUID: %s) has the characteristics:\n %s",
|
|
57
|
-
service.description,
|
|
58
|
-
service.uuid,
|
|
59
|
-
"\n".join(
|
|
60
|
-
f'{characteristic.uuid}: {characteristic.description}' for characteristic in service.characteristics
|
|
61
|
-
),
|
|
62
|
-
)
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
"""Tests for `ember_mug.utils`."""
|
|
2
|
-
|
|
3
|
-
from ember_mug.utils import (
|
|
4
|
-
bytes_to_big_int,
|
|
5
|
-
bytes_to_little_int,
|
|
6
|
-
decode_byte_string,
|
|
7
|
-
encode_byte_string,
|
|
8
|
-
temp_from_bytes,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def test_bytes_to_little_int() -> None:
|
|
13
|
-
assert bytes_to_little_int(b'\x05') == 5
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def test_bytes_to_big_int() -> None:
|
|
17
|
-
assert bytes_to_big_int(b'\x01\xc2') == 450
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def test_temp_from_bytes() -> None:
|
|
21
|
-
raw_data = bytearray(b'\xcd\x15') # int: 5581
|
|
22
|
-
assert temp_from_bytes(raw_data) == 55.81
|
|
23
|
-
assert temp_from_bytes(raw_data, metric=False) == 132.46
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_decode_byte_string() -> None:
|
|
27
|
-
assert decode_byte_string(b'abcd12345') == 'YWJjZDEyMzQ1'
|
|
28
|
-
assert decode_byte_string(b'') == ''
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_encode_byte_string() -> None:
|
|
32
|
-
assert encode_byte_string('abcd12345') == b'YWJjZDEyMzQ1'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|