python-ember-mug 1.3.0b5__tar.gz → 1.3.0b7__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.3.0b5
3
+ Version: 1.3.0b7
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/
@@ -6,4 +6,4 @@ __all__ = ("EmberMug",)
6
6
 
7
7
  __author__ = """Jesse Sopel"""
8
8
  __email__ = "jesse.sopel@gmail.com"
9
- __version__ = "1.3.0b5"
9
+ __version__ = "1.3.0b7"
@@ -2,13 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import re
6
5
  from argparse import ArgumentTypeError
7
6
  from collections import defaultdict
8
7
  from functools import partial
9
8
  from typing import TYPE_CHECKING
10
9
 
11
- from ember_mug.consts import MAC_ADDRESS_REGEX, TemperatureUnit
10
+ from ember_mug.consts import IS_MACOS, MAC_ADDRESS_REGEX, MAC_UUID_REGEX, TemperatureUnit
12
11
  from ember_mug.data import Change
13
12
  from ember_mug.formatting import format_led_colour, format_liquid_level, format_temp
14
13
 
@@ -26,7 +25,8 @@ base_formatters: dict[str, Callable] = {
26
25
 
27
26
  def validate_mac(value: str) -> str:
28
27
  """Check if specified MAC Address is valid."""
29
- if not isinstance(value, str) or not re.match(MAC_ADDRESS_REGEX, value):
28
+ mac_reg = MAC_UUID_REGEX if IS_MACOS else MAC_ADDRESS_REGEX
29
+ if not isinstance(value, str) or not mac_reg.match(value):
30
30
  raise ArgumentTypeError("Invalid MAC Address")
31
31
  return value.lower()
32
32
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import platform
6
6
  import re
7
- from enum import Enum, IntEnum
7
+ from enum import IntEnum, StrEnum
8
8
  from functools import cached_property
9
9
  from typing import NamedTuple
10
10
  from uuid import UUID
@@ -17,7 +17,7 @@ EMBER_BLE_SIG = 0x03C1
17
17
  DEFAULT_NAME = "Ember Device"
18
18
 
19
19
 
20
- class DeviceType(str, Enum):
20
+ class DeviceType(StrEnum):
21
21
  """Base device types."""
22
22
 
23
23
  CUP = "cup"
@@ -26,7 +26,7 @@ class DeviceType(str, Enum):
26
26
  TUMBLER = "tumbler"
27
27
 
28
28
 
29
- class DeviceModel(str, Enum):
29
+ class DeviceModel(StrEnum):
30
30
  """Know device models."""
31
31
 
32
32
  CUP_6_OZ = "CM21S"
@@ -50,7 +50,7 @@ DEVICE_MODEL_NAMES: dict[DeviceModel, str] = {
50
50
  }
51
51
 
52
52
 
53
- class DeviceColour(str, Enum):
53
+ class DeviceColour(StrEnum):
54
54
  """All colours possible found across models."""
55
55
 
56
56
  SAGE_GREEN = "Sage Green"
@@ -66,7 +66,7 @@ class DeviceColour(str, Enum):
66
66
  ROSE_GOLD = "Rose Gold"
67
67
 
68
68
 
69
- class TemperatureUnit(str, Enum):
69
+ class TemperatureUnit(StrEnum):
70
70
  """Temperature Units."""
71
71
 
72
72
  CELSIUS = "°C"
@@ -177,7 +177,7 @@ class LiquidState(IntEnum):
177
177
  return self.label
178
178
 
179
179
 
180
- class VolumeLevel(str, Enum):
180
+ class VolumeLevel(StrEnum):
181
181
  """Class to manage volume levels."""
182
182
 
183
183
  LOW = "low"
@@ -268,8 +268,11 @@ UPDATE_ATTRS = {
268
268
  EXTRA_ATTRS = {"battery_voltage", "date_time_zone", "udsk", "dsk"}
269
269
 
270
270
  # Validation
271
- MUG_NAME_REGEX = re.compile(r"^[A-Za-z0-9,.\[\]#()!\"\';:|\-_+<>%= ]{1,16}$")
271
+ # *Note*: Additional characters are escaped because Home Assistant uses LitElement with "v" mode which is stricter.
272
+ MUG_NAME_REGEX = re.compile(r"^[A-Za-z0-9,.\[\]#\(\)!\"\';:\|\-_+<>%= ]{1,16}$")
272
273
  MUG_NAME_PATTERN = MUG_NAME_REGEX.pattern
273
- MAC_ADDRESS_REGEX = re.compile(r"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$")
274
+ MAC_ADDRESS_REGEX = re.compile(r"^([0-9A-Fa-f]{2}:){5}([0-9A-Fa-f]{2})$", re.IGNORECASE)
275
+ MAC_UUID_REGEX = re.compile(r"^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$", re.IGNORECASE)
274
276
 
275
277
  IS_LINUX = platform.system() == "Linux"
278
+ IS_MACOS = platform.system() == "Darwin"
@@ -7,7 +7,7 @@ import contextlib
7
7
  import logging
8
8
  import os
9
9
  from datetime import UTC, datetime
10
- from enum import Enum
10
+ from enum import StrEnum
11
11
  from functools import cached_property
12
12
  from time import time
13
13
  from typing import TYPE_CHECKING, Any, Concatenate, Literal, ParamSpec, TypeVar
@@ -46,7 +46,7 @@ if TYPE_CHECKING:
46
46
  from bleak.backends.characteristic import BleakGATTCharacteristic
47
47
  from bleak.backends.device import BLEDevice
48
48
 
49
- TempUnitType = Literal["°C", "°F"] | TemperatureUnit | Enum
49
+ TempUnitType = Literal["°C", "°F"] | TemperatureUnit | StrEnum
50
50
 
51
51
 
52
52
  logger = logging.getLogger(__name__)
@@ -136,17 +136,13 @@ class EmberMug:
136
136
 
137
137
  def _convert_to_device_unit(self, value: float) -> float:
138
138
  """Convert user value to the unit the device expects."""
139
- if self.data.user_unit == TemperatureUnit.CELSIUS and self.data.temperature_unit == TemperatureUnit.FAHRENHEIT:
140
- return convert_temp_to_fahrenheit(value)
141
- if self.data.user_unit == TemperatureUnit.FAHRENHEIT and self.data.temperature_unit == TemperatureUnit.CELSIUS:
139
+ if self.data.user_unit == TemperatureUnit.FAHRENHEIT:
142
140
  return convert_temp_to_celsius(value)
143
141
  return value
144
142
 
145
143
  def _convert_to_user_unit(self, value: float) -> float:
146
144
  """Convert device value to the unit the user expects."""
147
- if self.data.user_unit == TemperatureUnit.CELSIUS and self.data.temperature_unit == TemperatureUnit.FAHRENHEIT:
148
- return convert_temp_to_celsius(value)
149
- if self.data.user_unit == TemperatureUnit.FAHRENHEIT and self.data.temperature_unit == TemperatureUnit.CELSIUS:
145
+ if self.data.user_unit == TemperatureUnit.FAHRENHEIT:
150
146
  return convert_temp_to_fahrenheit(value)
151
147
  return value
152
148
 
@@ -291,7 +287,7 @@ class EmberMug:
291
287
 
292
288
  async def pair(self) -> None:
293
289
  """Attempt to pair."""
294
- with contextlib.suppress(BleakError, EOFError):
290
+ with contextlib.suppress(BleakError, EOFError, NotImplementedError):
295
291
  await self._ensure_connection()
296
292
  await self._client.pair()
297
293
 
@@ -314,11 +310,11 @@ class EmberMug:
314
310
 
315
311
  async def set_target_temp(self, target_temp: float) -> None:
316
312
  """Set new target temp for mug."""
317
- if self.data.use_metric is not None:
318
- unit = TemperatureUnit.FAHRENHEIT if self.data.use_metric is False else TemperatureUnit.CELSIUS
319
- min_temp, max_temp = MIN_MAX_TEMPS[unit]
320
- if target_temp != 0 and not (min_temp <= target_temp <= max_temp):
321
- raise ValueError(f"Temperature should be between {min_temp} and {max_temp} or 0.")
313
+ unit = TemperatureUnit.FAHRENHEIT if self.data.use_metric is False else TemperatureUnit.CELSIUS
314
+ min_temp, max_temp = MIN_MAX_TEMPS[unit]
315
+ if target_temp != 0 and not (min_temp <= target_temp <= max_temp):
316
+ raise ValueError(f"Temperature '{target_temp}' is not between {min_temp} and {max_temp} or 0.")
317
+
322
318
  target_temp = self._convert_to_device_unit(target_temp)
323
319
  target = bytearray(round(target_temp / 0.01).to_bytes(2, "little"))
324
320
  await self._write(MugCharacteristic.TARGET_TEMPERATURE, target)
@@ -405,7 +401,7 @@ class EmberMug:
405
401
 
406
402
  async def set_temperature_unit(self, unit: TempUnitType) -> None:
407
403
  """Set mug unit."""
408
- text_unit = unit.value if isinstance(unit, Enum) else unit
404
+ text_unit = unit.value if isinstance(unit, StrEnum) else unit
409
405
  unit_bytes = bytearray([1 if text_unit == TemperatureUnit.FAHRENHEIT else 0])
410
406
  await self._write(MugCharacteristic.TEMPERATURE_UNIT, unit_bytes)
411
407
  self.data.temperature_unit = TemperatureUnit(unit)
@@ -67,13 +67,13 @@ def get_colour_from_int(colour_id: int) -> DeviceColour | None: # noqa: PLR0911
67
67
  return DeviceColour.BLACK
68
68
  if colour_id in (-126, -62, 2):
69
69
  return DeviceColour.WHITE
70
- if colour_id in (8, 11, -56, -63, -120, -117, -53):
70
+ if colour_id in (-120, -117, -63, -56, -53, 8, 11):
71
71
  return DeviceColour.RED
72
- if colour_id in (-131, -125, -61, 3, 83):
72
+ if colour_id in (-131, -125, -61, 3, 51, 83):
73
73
  return DeviceColour.COPPER
74
74
  if colour_id in (-124, -60):
75
75
  return DeviceColour.ROSE_GOLD
76
- if colour_id in (-59, -123):
76
+ if colour_id in (-123, -59):
77
77
  return DeviceColour.STAINLESS_STEEL
78
78
  return {
79
79
  -51: DeviceColour.SANDSTONE,
@@ -95,7 +95,7 @@ def get_model_from_single_int_and_services( # noqa PLR0911
95
95
  return DeviceModel.MUG_1_10_OZ
96
96
  if model_id == 65:
97
97
  return DeviceModel.MUG_1_14_OZ
98
- if model_id in (-51, -59, -63, -61, -62):
98
+ if model_id in (-51, -59, -63, -61, -62, 120):
99
99
  return DeviceModel.MUG_2_14_OZ
100
100
  if model_id == -60:
101
101
  return DeviceModel.CUP_6_OZ
@@ -108,7 +108,7 @@ def get_model_from_id_and_gen(model_id: int, generation: int) -> DeviceModel | N
108
108
  """Extract model from identifier in advertiser data."""
109
109
  if model_id == 1:
110
110
  return DeviceModel.MUG_1_10_OZ if generation < 2 else DeviceModel.MUG_2_10_OZ
111
- if model_id == 2:
111
+ if model_id in (2, 120):
112
112
  return DeviceModel.MUG_1_14_OZ if generation < 2 else DeviceModel.MUG_2_14_OZ
113
113
  if model_id == 3:
114
114
  return DeviceModel.TRAVEL_MUG_12_OZ
@@ -74,7 +74,6 @@ features = ["test"]
74
74
 
75
75
  [[tool.hatch.envs.test.matrix]]
76
76
  python = ["3.11", "3.12", "3.13", "3.14"]
77
- bleak = ["1.0.1", "2.0.0"]
78
77
 
79
78
  [tool.hatch.envs.test.scripts]
80
79
  cov = "pytest -vvv --asyncio-mode=auto --cov=ember_mug --cov-branch --cov-report=xml --cov-report=term-missing tests"