python-ember-mug 1.2.0b1__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.
Files changed (28) hide show
  1. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/PKG-INFO +7 -5
  2. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/README.md +3 -0
  3. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/__init__.py +1 -1
  4. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/cli/commands.py +10 -5
  5. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/cli/helpers.py +2 -2
  6. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/data.py +1 -1
  7. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/mug.py +25 -8
  8. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/utils.py +8 -10
  9. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/pyproject.toml +36 -42
  10. python_ember_mug-1.2.0b1/tests/__init__.py +0 -1
  11. python_ember_mug-1.2.0b1/tests/cli/__init__.py +0 -1
  12. python_ember_mug-1.2.0b1/tests/cli/test_commands.py +0 -380
  13. python_ember_mug-1.2.0b1/tests/cli/test_helpers.py +0 -92
  14. python_ember_mug-1.2.0b1/tests/conftest.py +0 -99
  15. python_ember_mug-1.2.0b1/tests/test_connection.py +0 -584
  16. python_ember_mug-1.2.0b1/tests/test_consts.py +0 -41
  17. python_ember_mug-1.2.0b1/tests/test_data.py +0 -115
  18. python_ember_mug-1.2.0b1/tests/test_formatting.py +0 -24
  19. python_ember_mug-1.2.0b1/tests/test_mug_data.py +0 -120
  20. python_ember_mug-1.2.0b1/tests/test_scanner.py +0 -71
  21. python_ember_mug-1.2.0b1/tests/test_utils.py +0 -224
  22. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/.gitignore +0 -0
  23. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/LICENSE +0 -0
  24. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/__main__.py +0 -0
  25. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/cli/__init__.py +0 -0
  26. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/consts.py +0 -0
  27. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/formatting.py +0 -0
  28. {python_ember_mug-1.2.0b1 → python_ember_mug-1.3.0b1}/ember_mug/scanner.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-ember-mug
3
- Version: 1.2.0b1
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/
7
7
  Project-URL: Source code, https://github.com/sopelj/python-ember-mug/
8
8
  Project-URL: Bug Tracker, https://github.com/sopelj/python-ember-mug/issues
9
- Author-email: Jesse Sopel <jesse.sopel@gmail.com>
9
+ Author-email: Jesse Sopel <jesse@sopelj.ca>
10
10
  License-Expression: MIT
11
11
  License-File: LICENSE
12
12
  Classifier: Intended Audience :: Developers
@@ -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.0b1"
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,15 +56,12 @@ 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
- def get_colour_from_int(colour_id: int) -> DeviceColour | None:
64
+ def get_colour_from_int(colour_id: int) -> DeviceColour | None: # noqa: PLR0911
68
65
  """Extrapolate device colour from integer in advertiser data."""
69
66
  if colour_id in (-127, -63, 1, 14, 65):
70
67
  return DeviceColour.BLACK
@@ -76,13 +73,14 @@ def get_colour_from_int(colour_id: int) -> DeviceColour | None:
76
73
  return DeviceColour.COPPER
77
74
  if colour_id in (-124, -60):
78
75
  return DeviceColour.ROSE_GOLD
76
+ if colour_id in (-59, -123):
77
+ return DeviceColour.STAINLESS_STEEL
79
78
  return {
80
79
  -51: DeviceColour.SANDSTONE,
81
80
  -52: DeviceColour.SAGE_GREEN,
82
81
  -55: DeviceColour.GREY,
83
82
  -57: DeviceColour.BLUE,
84
83
  -122: DeviceColour.GOLD,
85
- -123: DeviceColour.STAINLESS_STEEL,
86
84
  }.get(colour_id)
87
85
 
88
86
 
@@ -97,7 +95,7 @@ def get_model_from_single_int_and_services( # noqa PLR0911
97
95
  return DeviceModel.MUG_1_10_OZ
98
96
  if model_id == 65:
99
97
  return DeviceModel.MUG_1_14_OZ
100
- if model_id in (-51, -63, -61, -62):
98
+ if model_id in (-51, -59, -63, -61, -62):
101
99
  return DeviceModel.MUG_2_14_OZ
102
100
  if model_id == -60:
103
101
  return DeviceModel.CUP_6_OZ
@@ -150,7 +148,7 @@ def get_model_info_from_advertiser_data(advertisement: AdvertisementData) -> Mod
150
148
  get_colour_from_int(colour_id),
151
149
  )
152
150
  logger.debug(
153
- "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.",
154
152
  )
155
153
  return ModelInfo(guess_model_from_name(advertisement.local_name))
156
154
 
@@ -2,13 +2,11 @@
2
2
  name = "python-ember-mug"
3
3
  readme = "README.md"
4
4
  description = "Python Library for Ember Mugs."
5
- authors = [
6
- { name = "Jesse Sopel", email = "jesse.sopel@gmail.com" },
7
- ]
8
- license = "MIT"
5
+ authors = [{ name = "Jesse Sopel", email = "jesse@sopelj.ca" }]
6
+ license = "MIT"
9
7
  requires-python = ">=3.11"
10
8
  dynamic = ["version"]
11
- classifiers=[
9
+ classifiers = [
12
10
  'Intended Audience :: Developers',
13
11
  'License :: OSI Approved :: MIT License',
14
12
  'Natural Language :: English',
@@ -18,32 +16,25 @@ classifiers=[
18
16
  'Programming Language :: Python :: 3.13',
19
17
  ]
20
18
  dependencies = [
21
- "bleak-retry-connector>=3.5.0",
22
- "bleak>=0.22.2; python_version < '3.13'",
23
- "bleak>=0.22.3; python_version <= '3.13'",
19
+ "bleak-retry-connector>=4.0.2",
20
+ "bleak>=1.0.1",
24
21
  ]
25
22
 
26
23
  [project.optional-dependencies]
27
- test = [
28
- "pytest>=7.2.1",
29
- "pytest-cov",
30
- "pytest-asyncio",
31
- ]
24
+ test = ["pytest>=7.2.1", "pytest-cov", "pytest-asyncio"]
32
25
  docs = [
33
- "mkdocs>=1.6.1",
34
- "mkdocs-include-markdown-plugin>=7.0.0,<8.0.0",
35
- "mkdocs-material>=9.5.44,<10.0.0",
36
- "mkdocs-material-extensions",
37
- "mkdocstrings-python>=1.12.0",
38
- "mkdocs-autorefs",
39
- "mkdocs-literate-nav",
40
- "mkdocs-gen-files",
41
- "black",
42
- "termynal",
43
- ]
44
- dev = [
45
- "ipython",
26
+ "mkdocs>=1.6.1",
27
+ "mkdocs-include-markdown-plugin>=7.0.0,<8.0.0",
28
+ "mkdocs-material>=9.5.44,<10.0.0",
29
+ "mkdocs-material-extensions",
30
+ "mkdocstrings-python>=1.12.0",
31
+ "mkdocs-autorefs",
32
+ "mkdocs-literate-nav",
33
+ "mkdocs-gen-files",
34
+ "black",
35
+ "termynal",
46
36
  ]
37
+ dev = ["ipython"]
47
38
 
48
39
  [project.urls]
49
40
  "Changelog" = "https://sopelj.github.io/python-ember-mug/changelog/"
@@ -63,16 +54,19 @@ build-backend = "hatchling.build"
63
54
  [tool.hatch.version]
64
55
  path = "ember_mug/__init__.py"
65
56
 
57
+ [tool.hatch.build]
58
+ exclude = ["/docs", "/tests"]
59
+
66
60
  [tool.hatch.build.targets.sdist]
67
- packages = ["ember_mug", "tests"]
61
+ packages = ["ember_mug"]
68
62
  exclude = [".gitignore"]
69
63
 
70
64
  [tool.hatch.build.targets.wheel]
71
- packages = ["ember_mug", "tests"]
65
+ packages = ["ember_mug"]
72
66
  exclude = [".gitignore"]
73
67
 
74
68
  [tool.hatch.envs.default]
75
- python = "3.12"
69
+ python = "3.13"
76
70
 
77
71
  [tool.hatch.envs.test]
78
72
  features = ["test"]
@@ -85,7 +79,7 @@ cov = "pytest -vvv --asyncio-mode=auto --cov=ember_mug --cov-branch --cov-report
85
79
  no-cov = "cov --no-cov"
86
80
 
87
81
  [tool.hatch.envs.docs]
88
- python = "3.12"
82
+ python = "3.13"
89
83
  features = ["docs"]
90
84
 
91
85
  [tool.hatch.envs.docs.scripts]
@@ -95,7 +89,7 @@ serve = "mkdocs serve --dev-addr localhost:8000"
95
89
  [tool.black]
96
90
  line-length = 120
97
91
  skip-string-normalization = true
98
- target-version = ["py311", "py312"]
92
+ target-version = ["py311", "py312", "py313"]
99
93
  include = '\.pyi?$'
100
94
  exclude = '''
101
95
  /(
@@ -112,15 +106,15 @@ exclude = '''
112
106
 
113
107
  [tool.coverage.report]
114
108
  exclude_lines = [
115
- "pragma: no cover",
116
- "def __repr__",
117
- "def __str__",
118
- "def main",
119
- "raise AssertionError",
120
- "raise NotImplementedError",
121
- "if __name__ == .__main__.:",
122
- "if TYPE_CHECKING:",
123
- "if typing.TYPE_CHECKING:"
109
+ "pragma: no cover",
110
+ "def __repr__",
111
+ "def __str__",
112
+ "def main",
113
+ "raise AssertionError",
114
+ "raise NotImplementedError",
115
+ "if __name__ == .__main__.:",
116
+ "if TYPE_CHECKING:",
117
+ "if typing.TYPE_CHECKING:",
124
118
  ]
125
119
 
126
120
  [tool.ruff]
@@ -163,7 +157,7 @@ ignore = ["D203", "D212", "PLR2004"]
163
157
 
164
158
  [tool.ruff.lint.per-file-ignores]
165
159
  "tests/**/*.py" = [
166
- "D103", # No docstrings in tests needed
167
- "S101", # We needs asserts in tests
160
+ "D103", # No docstrings in tests needed
161
+ "S101", # We needs asserts in tests
168
162
  "SLF001",
169
163
  ]
@@ -1 +0,0 @@
1
- """Unit test package for python-ember-mug."""
@@ -1 +0,0 @@
1
- """Tests for the CLI module."""