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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-ember-mug
3
- Version: 1.2.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>=3.5.0
21
- Requires-Dist: bleak>=0.22.2; python_version < '3.13'
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
 
@@ -6,4 +6,4 @@ __all__ = ("EmberMug",)
6
6
 
7
7
  __author__ = """Jesse Sopel"""
8
8
  __email__ = "jesse.sopel@gmail.com"
9
- __version__ = "1.2.1"
9
+ __version__ = "1.3.0b1"
@@ -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
- get_model_info_from_advertiser_data(advertisement),
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'Options: {", ".join(options)}')
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'set_{attr.replace("-", "_")}')
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'+{"+".join("-" * i for i in column_sizes)}+'
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'{self.percent}%, {"" if self.on_charging_base else "not "}on charging base'
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 is True:
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, self.data.use_metric)
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
- if self.data.use_metric is False:
286
- target_temp = convert_temp_to_celsius(target_temp)
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, self.data.use_metric)
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: Literal["°C", "°F"] | TemperatureUnit | Enum) -> None:
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, metric: bool = True) -> float:
60
- """Get temperature from bytearray and convert to Fahrenheit if needed."""
61
- temp = float(bytes_to_little_int(temp_bytes)) * 0.01
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." "Falling back to guessing based on name.",
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>=3.5.0",
20
- "bleak>=0.22.2; python_version < '3.13'",
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.12"
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.12"
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
  /(