python-ember-mug 1.2.1__tar.gz → 1.3.0b1__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-1.2.1 → python_ember_mug-1.3.0b1}/PKG-INFO +6 -4
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/README.md +3 -0
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/__init__.py +1 -1
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/cli/commands.py +10 -5
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/cli/helpers.py +2 -2
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/data.py +1 -1
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/mug.py +25 -8
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/utils.py +4 -7
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/pyproject.toml +5 -6
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/.gitignore +0 -0
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/LICENSE +0 -0
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/__main__.py +0 -0
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/cli/__init__.py +0 -0
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/consts.py +0 -0
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/formatting.py +0 -0
- {python_ember_mug-1.2.1 → python_ember_mug-1.3.0b1}/ember_mug/scanner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-ember-mug
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0b1
|
|
4
4
|
Summary: Python Library for Ember Mugs.
|
|
5
5
|
Project-URL: Changelog, https://sopelj.github.io/python-ember-mug/changelog/
|
|
6
6
|
Project-URL: Documentation, https://sopelj.github.io/python-ember-mug/
|
|
@@ -17,9 +17,8 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Requires-Python: >=3.11
|
|
20
|
-
Requires-Dist: bleak-retry-connector>=
|
|
21
|
-
Requires-Dist: bleak>=0.
|
|
22
|
-
Requires-Dist: bleak>=0.22.3; python_version <= '3.13'
|
|
20
|
+
Requires-Dist: bleak-retry-connector>=4.0.2
|
|
21
|
+
Requires-Dist: bleak>=1.0.1
|
|
23
22
|
Provides-Extra: dev
|
|
24
23
|
Requires-Dist: ipython; extra == 'dev'
|
|
25
24
|
Provides-Extra: docs
|
|
@@ -207,6 +206,9 @@ You may also need to re-add it to the app in order to make it writable again as
|
|
|
207
206
|
This seems to be caused by the bluetooth adaptor being in some sort of passive mode. I have not yet figured out how to wake it programmatically so sadly, you need to manually open `bluetoothctl` to do so.
|
|
208
207
|
Please ensure the device is in pairing mode (ie the light is flashing blue or says "PAIR") and run the `bluetoothctl` command. You don't need to type anything. run it and wait until the mug connects.
|
|
209
208
|
|
|
209
|
+
### Model incorrect or not found
|
|
210
|
+
|
|
211
|
+
I don't have a lot of these devices, so if this library does not correctly identify your device, please open and issue with the advertisement data of your device, so I can update the library to correctly identify it. Thanks!
|
|
210
212
|
|
|
211
213
|
## Development
|
|
212
214
|
|
|
@@ -166,6 +166,9 @@ You may also need to re-add it to the app in order to make it writable again as
|
|
|
166
166
|
This seems to be caused by the bluetooth adaptor being in some sort of passive mode. I have not yet figured out how to wake it programmatically so sadly, you need to manually open `bluetoothctl` to do so.
|
|
167
167
|
Please ensure the device is in pairing mode (ie the light is flashing blue or says "PAIR") and run the `bluetoothctl` command. You don't need to type anything. run it and wait until the mug connects.
|
|
168
168
|
|
|
169
|
+
### Model incorrect or not found
|
|
170
|
+
|
|
171
|
+
I don't have a lot of these devices, so if this library does not correctly identify your device, please open and issue with the advertisement data of your device, so I can update the library to correctly identify it. Thanks!
|
|
169
172
|
|
|
170
173
|
## Development
|
|
171
174
|
|
|
@@ -12,8 +12,8 @@ from typing import TYPE_CHECKING, ClassVar
|
|
|
12
12
|
|
|
13
13
|
from bleak import AdvertisementData, BleakError
|
|
14
14
|
|
|
15
|
-
from ember_mug.consts import ATTR_LABELS, EXTRA_ATTRS, IS_LINUX, VolumeLevel
|
|
16
|
-
from ember_mug.data import Colour
|
|
15
|
+
from ember_mug.consts import ATTR_LABELS, EMBER_BLE_SIG, EXTRA_ATTRS, IS_LINUX, VolumeLevel
|
|
16
|
+
from ember_mug.data import Colour, DeviceModel
|
|
17
17
|
from ember_mug.mug import EmberMug
|
|
18
18
|
from ember_mug.scanner import discover_devices, find_device
|
|
19
19
|
|
|
@@ -33,9 +33,14 @@ get_attribute_names = [n.replace("_", "-") for n in all_attrs]
|
|
|
33
33
|
async def get_device(args: Namespace) -> EmberMug:
|
|
34
34
|
"""Help to get the devices based on command args."""
|
|
35
35
|
device, advertisement = await find_device_cmd(args)
|
|
36
|
+
model_info = get_model_info_from_advertiser_data(advertisement)
|
|
37
|
+
if model_info.model == DeviceModel.UNKNOWN_DEVICE and not args.raw:
|
|
38
|
+
data = advertisement.manufacturer_data.get(EMBER_BLE_SIG, None)
|
|
39
|
+
print(f"Warning: No model found matching advertisement data: {data!r}")
|
|
40
|
+
|
|
36
41
|
mug = EmberMug(
|
|
37
42
|
device,
|
|
38
|
-
|
|
43
|
+
model_info,
|
|
39
44
|
use_metric=not args.imperial,
|
|
40
45
|
debug=args.debug,
|
|
41
46
|
)
|
|
@@ -139,13 +144,13 @@ async def set_device_value_cmd(args: Namespace) -> None:
|
|
|
139
144
|
if not values:
|
|
140
145
|
print("Please specify at least one attribute and value to set.")
|
|
141
146
|
options = [f"--{a.replace('_', '-')}" for a in attrs]
|
|
142
|
-
print(f
|
|
147
|
+
print(f"Options: {', '.join(options)}")
|
|
143
148
|
sys.exit(1)
|
|
144
149
|
|
|
145
150
|
mug = await get_device(args)
|
|
146
151
|
async with mug.connection(adapter=args.adapter):
|
|
147
152
|
for attr, value in values:
|
|
148
|
-
method = getattr(mug, f
|
|
153
|
+
method = getattr(mug, f"set_{attr.replace('-', '_')}")
|
|
149
154
|
print(f"Setting {attr} to {value}")
|
|
150
155
|
try:
|
|
151
156
|
await method(value)
|
|
@@ -47,11 +47,11 @@ def print_table(data: list[tuple[str, ...]]) -> None:
|
|
|
47
47
|
rows = [build_sub_rows(r) for r in data]
|
|
48
48
|
num_columns = max(len(sr) for r in rows for sr in r.values())
|
|
49
49
|
column_sizes = [max(len(sr[i]) for r in rows for sr in r.values()) + 2 for i in range(num_columns)]
|
|
50
|
-
vertical = f
|
|
50
|
+
vertical = f"+{'+'.join('-' * i for i in column_sizes)}+"
|
|
51
51
|
print(vertical)
|
|
52
52
|
for row in rows:
|
|
53
53
|
for sub_row in row.values():
|
|
54
|
-
inner = "|".join(f" {sub_row[i]:<{width-2}} " for i, width in enumerate(column_sizes))
|
|
54
|
+
inner = "|".join(f" {sub_row[i]:<{width - 2}} " for i, width in enumerate(column_sizes))
|
|
55
55
|
print(f"|{inner}|")
|
|
56
56
|
print(vertical)
|
|
57
57
|
|
|
@@ -87,7 +87,7 @@ class BatteryInfo(AsDict):
|
|
|
87
87
|
|
|
88
88
|
def __str__(self) -> str:
|
|
89
89
|
"""Format nicely for printing."""
|
|
90
|
-
return f
|
|
90
|
+
return f"{self.percent}%, {'' if self.on_charging_base else 'not '}on charging base"
|
|
91
91
|
|
|
92
92
|
|
|
93
93
|
@dataclass
|
|
@@ -31,6 +31,7 @@ from .utils import (
|
|
|
31
31
|
bytes_to_big_int,
|
|
32
32
|
bytes_to_little_int,
|
|
33
33
|
convert_temp_to_celsius,
|
|
34
|
+
convert_temp_to_fahrenheit,
|
|
34
35
|
decode_byte_string,
|
|
35
36
|
discover_services,
|
|
36
37
|
encode_byte_string,
|
|
@@ -44,6 +45,8 @@ if TYPE_CHECKING:
|
|
|
44
45
|
from bleak.backends.characteristic import BleakGATTCharacteristic
|
|
45
46
|
from bleak.backends.device import BLEDevice
|
|
46
47
|
|
|
48
|
+
TempUnitType = Literal["°C", "°F"] | TemperatureUnit | Enum
|
|
49
|
+
|
|
47
50
|
|
|
48
51
|
logger = logging.getLogger(__name__)
|
|
49
52
|
|
|
@@ -130,6 +133,22 @@ class EmberMug:
|
|
|
130
133
|
"""Check if the mug can support write operations."""
|
|
131
134
|
return self.data.udsk is not None
|
|
132
135
|
|
|
136
|
+
def _convert_to_device_unit(self, value: float) -> float:
|
|
137
|
+
"""Convert user value to the unit the device expects."""
|
|
138
|
+
if self.data.use_metric and self.data.temperature_unit != TemperatureUnit.CELSIUS:
|
|
139
|
+
return convert_temp_to_fahrenheit(value)
|
|
140
|
+
if not self.data.use_metric and self.data.temperature_unit != TemperatureUnit.FAHRENHEIT:
|
|
141
|
+
return convert_temp_to_celsius(value)
|
|
142
|
+
return value
|
|
143
|
+
|
|
144
|
+
def _convert_to_user_unit(self, value: float) -> float:
|
|
145
|
+
"""Convert device value to the unit the user expects."""
|
|
146
|
+
if self.data.use_metric and self.data.temperature_unit != TemperatureUnit.CELSIUS:
|
|
147
|
+
return convert_temp_to_celsius(value)
|
|
148
|
+
if not self.data.use_metric and self.data.temperature_unit != TemperatureUnit.FAHRENHEIT:
|
|
149
|
+
return convert_temp_to_fahrenheit(value)
|
|
150
|
+
return value
|
|
151
|
+
|
|
133
152
|
def has_attribute(self, attribute: str) -> bool:
|
|
134
153
|
"""Check whether the device has the given attribute."""
|
|
135
154
|
return attribute in self.data.model_info.device_attributes
|
|
@@ -155,7 +174,7 @@ class EmberMug:
|
|
|
155
174
|
disconnected_callback=self._disconnect_callback,
|
|
156
175
|
ble_device_callback=lambda: self.device,
|
|
157
176
|
)
|
|
158
|
-
if self.debug
|
|
177
|
+
if self.debug:
|
|
159
178
|
await discover_services(client)
|
|
160
179
|
self._expected_disconnect = False
|
|
161
180
|
except (TimeoutError, BleakError) as error:
|
|
@@ -273,7 +292,7 @@ class EmberMug:
|
|
|
273
292
|
async def get_target_temp(self) -> float:
|
|
274
293
|
"""Get target temp form mug gatt."""
|
|
275
294
|
temp_bytes = await self._read(MugCharacteristic.TARGET_TEMPERATURE)
|
|
276
|
-
return temp_from_bytes(temp_bytes
|
|
295
|
+
return self._convert_to_user_unit(temp_from_bytes(temp_bytes))
|
|
277
296
|
|
|
278
297
|
async def set_target_temp(self, target_temp: float) -> None:
|
|
279
298
|
"""Set new target temp for mug."""
|
|
@@ -282,17 +301,15 @@ class EmberMug:
|
|
|
282
301
|
if target_temp != 0 and not (min_temp <= target_temp <= max_temp):
|
|
283
302
|
raise ValueError(f"Temperature should be between {min_temp} and {max_temp} or 0.")
|
|
284
303
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
target = bytearray(int(target_temp / 0.01).to_bytes(2, "little"))
|
|
304
|
+
target_temp = self._convert_to_device_unit(target_temp)
|
|
305
|
+
target = bytearray(round(target_temp / 0.01).to_bytes(2, "little"))
|
|
289
306
|
await self._write(MugCharacteristic.TARGET_TEMPERATURE, target)
|
|
290
307
|
self.data.target_temp = target_temp
|
|
291
308
|
|
|
292
309
|
async def get_current_temp(self) -> float:
|
|
293
310
|
"""Get current temp from mug gatt."""
|
|
294
311
|
temp_bytes = await self._read(MugCharacteristic.CURRENT_TEMPERATURE)
|
|
295
|
-
return temp_from_bytes(temp_bytes
|
|
312
|
+
return self._convert_to_user_unit(temp_from_bytes(temp_bytes))
|
|
296
313
|
|
|
297
314
|
async def get_liquid_level(self) -> int:
|
|
298
315
|
"""Get liquid level from mug gatt."""
|
|
@@ -368,7 +385,7 @@ class EmberMug:
|
|
|
368
385
|
return TemperatureUnit.CELSIUS
|
|
369
386
|
return TemperatureUnit.FAHRENHEIT
|
|
370
387
|
|
|
371
|
-
async def set_temperature_unit(self, unit:
|
|
388
|
+
async def set_temperature_unit(self, unit: TempUnitType) -> None:
|
|
372
389
|
"""Set mug unit."""
|
|
373
390
|
text_unit = unit.value if isinstance(unit, Enum) else unit
|
|
374
391
|
unit_bytes = bytearray([1 if text_unit == TemperatureUnit.FAHRENHEIT else 0])
|
|
@@ -56,12 +56,9 @@ def convert_temp_to_celsius(temp: float) -> float:
|
|
|
56
56
|
return (temp - 32) * 5 / 9
|
|
57
57
|
|
|
58
58
|
|
|
59
|
-
def temp_from_bytes(temp_bytes: bytearray
|
|
60
|
-
"""Get temperature from bytearray
|
|
61
|
-
|
|
62
|
-
if metric is False:
|
|
63
|
-
temp = convert_temp_to_fahrenheit(temp)
|
|
64
|
-
return round(temp, 2)
|
|
59
|
+
def temp_from_bytes(temp_bytes: bytearray) -> float:
|
|
60
|
+
"""Get temperature from bytearray."""
|
|
61
|
+
return float(bytes_to_little_int(temp_bytes)) * 0.01
|
|
65
62
|
|
|
66
63
|
|
|
67
64
|
def get_colour_from_int(colour_id: int) -> DeviceColour | None: # noqa: PLR0911
|
|
@@ -151,7 +148,7 @@ def get_model_info_from_advertiser_data(advertisement: AdvertisementData) -> Mod
|
|
|
151
148
|
get_colour_from_int(colour_id),
|
|
152
149
|
)
|
|
153
150
|
logger.debug(
|
|
154
|
-
"Unable to reliably determine model info from advertiser data.
|
|
151
|
+
"Unable to reliably determine model info from advertiser data.Falling back to guessing based on name.",
|
|
155
152
|
)
|
|
156
153
|
return ModelInfo(guess_model_from_name(advertisement.local_name))
|
|
157
154
|
|
|
@@ -16,9 +16,8 @@ classifiers = [
|
|
|
16
16
|
'Programming Language :: Python :: 3.13',
|
|
17
17
|
]
|
|
18
18
|
dependencies = [
|
|
19
|
-
"bleak-retry-connector>=
|
|
20
|
-
"bleak>=0.
|
|
21
|
-
"bleak>=0.22.3; python_version <= '3.13'",
|
|
19
|
+
"bleak-retry-connector>=4.0.2",
|
|
20
|
+
"bleak>=1.0.1",
|
|
22
21
|
]
|
|
23
22
|
|
|
24
23
|
[project.optional-dependencies]
|
|
@@ -67,7 +66,7 @@ packages = ["ember_mug"]
|
|
|
67
66
|
exclude = [".gitignore"]
|
|
68
67
|
|
|
69
68
|
[tool.hatch.envs.default]
|
|
70
|
-
python = "3.
|
|
69
|
+
python = "3.13"
|
|
71
70
|
|
|
72
71
|
[tool.hatch.envs.test]
|
|
73
72
|
features = ["test"]
|
|
@@ -80,7 +79,7 @@ cov = "pytest -vvv --asyncio-mode=auto --cov=ember_mug --cov-branch --cov-report
|
|
|
80
79
|
no-cov = "cov --no-cov"
|
|
81
80
|
|
|
82
81
|
[tool.hatch.envs.docs]
|
|
83
|
-
python = "3.
|
|
82
|
+
python = "3.13"
|
|
84
83
|
features = ["docs"]
|
|
85
84
|
|
|
86
85
|
[tool.hatch.envs.docs.scripts]
|
|
@@ -90,7 +89,7 @@ serve = "mkdocs serve --dev-addr localhost:8000"
|
|
|
90
89
|
[tool.black]
|
|
91
90
|
line-length = 120
|
|
92
91
|
skip-string-normalization = true
|
|
93
|
-
target-version = ["py311", "py312"]
|
|
92
|
+
target-version = ["py311", "py312", "py313"]
|
|
94
93
|
include = '\.pyi?$'
|
|
95
94
|
exclude = '''
|
|
96
95
|
/(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|