python-ember-mug 0.8.1__tar.gz → 0.9.0__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 (33) hide show
  1. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/PKG-INFO +33 -24
  2. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/README.md +28 -21
  3. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/__init__.py +1 -1
  4. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/cli/commands.py +37 -18
  5. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/cli/helpers.py +2 -2
  6. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/consts.py +71 -22
  7. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/data.py +72 -63
  8. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/formatting.py +11 -0
  9. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/mug.py +71 -42
  10. python_ember_mug-0.9.0/ember_mug/scanner.py +68 -0
  11. python_ember_mug-0.9.0/ember_mug/utils.py +195 -0
  12. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/pyproject.toml +13 -6
  13. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/cli/test_commands.py +105 -17
  14. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/cli/test_helpers.py +3 -2
  15. python_ember_mug-0.9.0/tests/conftest.py +92 -0
  16. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_connection.py +165 -131
  17. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_consts.py +1 -1
  18. python_ember_mug-0.9.0/tests/test_data.py +78 -0
  19. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_formatting.py +7 -1
  20. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_mug_data.py +28 -14
  21. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_scanner.py +25 -23
  22. python_ember_mug-0.9.0/tests/test_utils.py +179 -0
  23. python_ember_mug-0.8.1/ember_mug/scanner.py +0 -60
  24. python_ember_mug-0.8.1/ember_mug/utils.py +0 -100
  25. python_ember_mug-0.8.1/tests/conftest.py +0 -47
  26. python_ember_mug-0.8.1/tests/test_data.py +0 -75
  27. python_ember_mug-0.8.1/tests/test_utils.py +0 -70
  28. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/.gitignore +0 -0
  29. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/LICENSE +0 -0
  30. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/__main__.py +0 -0
  31. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/cli/__init__.py +0 -0
  32. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/__init__.py +0 -0
  33. {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/cli/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-ember-mug
3
- Version: 0.8.1
3
+ Version: 0.9.0
4
4
  Summary: Python Library for Ember Mugs.
5
5
  Project-URL: Documentation, https://sopelj.github.io/python-ember-mug/
6
6
  Project-URL: Source code, https://github.com/sopelj/python-ember-mug/
@@ -15,9 +15,11 @@ Classifier: Natural Language :: English
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Programming Language :: Python :: 3.10
17
17
  Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
18
19
  Requires-Python: >=3.10
19
- Requires-Dist: bleak-retry-connector>=3.0.0
20
- Requires-Dist: bleak>=0.20.2
20
+ Requires-Dist: async-timeout; python_version < '3.11'
21
+ Requires-Dist: bleak-retry-connector>=3.1.2
22
+ Requires-Dist: bleak>=0.21.0
21
23
  Provides-Extra: docs
22
24
  Requires-Dist: mkdocs-autorefs; extra == 'docs'
23
25
  Requires-Dist: mkdocs-include-markdown-plugin<7.0.0,>=3.6.1; extra == 'docs'
@@ -37,7 +39,7 @@ Description-Content-Type: text/markdown
37
39
  [![python](https://img.shields.io/pypi/pyversions/python-ember-mug.svg)](https://pypi.org/project/python-ember-mug/)
38
40
  [![Build Status](https://github.com/sopelj/python-ember-mug/actions/workflows/tests.yml/badge.svg)](https://github.com/sopelj/python-ember-mug/actions/workflows/tests.yml)
39
41
  [![codecov](https://codecov.io/gh/sopelj/python-ember-mug/branch/main/graphs/badge.svg)](https://codecov.io/github/sopelj/python-ember-mug)
40
- ![Project Maintenance](https://img.shields.io/maintenance/yes/2023.svg)
42
+ ![Project Maintenance](https://img.shields.io/maintenance/yes/2024.svg)
41
43
  [![Maintainer](https://img.shields.io/badge/maintainer-%40sopelj-blue.svg)](https://github.com/sopelj)
42
44
  [![License](https://img.shields.io/github/license/sopelj/python-ember-mug.svg)](LICENSE)
43
45
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen)](https://github.com/pre-commit/pre-commit)
@@ -54,16 +56,17 @@ This is an *unofficial* library to attempt to interact with Ember Mugs via Bluet
54
56
  This was created for use with my [Home Assistant integration](https://github.com/sopelj/hass-ember-mug-component),
55
57
  but could be useful separately and has a simple CLI interface too.
56
58
 
57
- All known Ember Mugs, Cups and Travel Mugs have been tested and work.
58
- If I missed one, or you have new feature ideas/issues, please let me know.
59
+ All known Ember Mugs, Cups, Tumblers and Travel Mugs have been tested and seem to work well.
60
+ If I missed one, or you have new feature ideas or issues, please [create an issue](https://github.com/sopelj/python-ember-mug/issues), if it isn't already there, and we'll figure it out.
59
61
 
60
- | Device | Tested |
61
- |--------------|---------|
62
- | Mug | ✓ |
63
- | Mug 2 | ✓ |
64
- | Cup | ✓ |
65
- | Travel Mug | ✓ |
66
- | Travel Mug 2 | ✓ |
62
+ | Device | Tested |
63
+ |--------------|--------|
64
+ | Mug | ✓ |
65
+ | Mug 2 | ✓ |
66
+ | Cup | ✓ |
67
+ | Tumbler | ✓ |
68
+ | Travel Mug | ✓ |
69
+ | Travel Mug 2 | ✓ |
67
70
 
68
71
  ## Features
69
72
 
@@ -74,17 +77,17 @@ If I missed one, or you have new feature ideas/issues, please let me know.
74
77
 
75
78
  Attributes by device:
76
79
 
77
- | Attribute | Mug | Cup | Travel Mug | Description |
78
- |---------------------|-----|-----|------------|-----------------------------------------------|
79
- | Name | R/W | N/A | R | Name to give device |
80
- | LED Colour | R/W | R/W | N/A | Colour of front LED |
81
- | Current Temperature | R | R | R | Current temperature of the liquid in the mug |
82
- | Target Temperature | R/W | R/W | R/W | Desired temperature for the liquid |
83
- | Temperature Unit | R/W | R/W | R/W | Internal temperature unit for the app (C/F) |
84
- | Liquid Level | R | R | R | Approximate level of the liquid in the device |
85
- | Volume level | N/A | N/A | R/W | Volume of the button press beep |
86
- | Battery Percent | R | R | R | Current battery level |
87
- | On Charger | R | R | R | Device is on it's charger |
80
+ | Attribute | Mug | Cup | Tumbler | Travel Mug | Description |
81
+ |---------------------|-----|-----|---------|------------|-----------------------------------------------|
82
+ | Name | R/W | N/A | N/A | R | Name to give device |
83
+ | LED Colour | R/W | R/W | R/W | N/A | Colour of front LED |
84
+ | Current Temperature | R | R | R | R | Current temperature of the liquid in the mug |
85
+ | Target Temperature | R/W | R/W | R/W | R/W | Desired temperature for the liquid |
86
+ | Temperature Unit | R/W | R/W | R/W | R/W | Internal temperature unit for the app (C/F) |
87
+ | Liquid Level | R | R | R | R | Approximate level of the liquid in the device |
88
+ | Volume level | N/A | N/A | N/A | R/W | Volume of the button press beep |
89
+ | Battery Percent | R | R | R | R | Current battery level |
90
+ | On Charger | R | R | R | R | Device is on it's charger |
88
91
 
89
92
  *** Writing may only work if the devices has been set up in the app previously
90
93
 
@@ -148,6 +151,12 @@ Basic options:
148
151
 
149
152
  ## Troubleshooting
150
153
 
154
+ ##### Systematic timeouts or `le-connection-abort-by-local`
155
+
156
+ If your mug gets stuck in a state where it refuses to connect, you get constant reconnects, timeouts, and/or `le-connection-abort-by-local` messages in the debug logs, you may need to remove
157
+ your mug via `bluetoothctl remove my-mac-address` and factory reset your device. It should reconnect correctly afterward.
158
+ You may also need to re-add it to the app in order to make it writable again as well.
159
+
151
160
  ### 'Operation failed with ATT error: 0x0e' or another connection error
152
161
 
153
162
  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.
@@ -4,7 +4,7 @@
4
4
  [![python](https://img.shields.io/pypi/pyversions/python-ember-mug.svg)](https://pypi.org/project/python-ember-mug/)
5
5
  [![Build Status](https://github.com/sopelj/python-ember-mug/actions/workflows/tests.yml/badge.svg)](https://github.com/sopelj/python-ember-mug/actions/workflows/tests.yml)
6
6
  [![codecov](https://codecov.io/gh/sopelj/python-ember-mug/branch/main/graphs/badge.svg)](https://codecov.io/github/sopelj/python-ember-mug)
7
- ![Project Maintenance](https://img.shields.io/maintenance/yes/2023.svg)
7
+ ![Project Maintenance](https://img.shields.io/maintenance/yes/2024.svg)
8
8
  [![Maintainer](https://img.shields.io/badge/maintainer-%40sopelj-blue.svg)](https://github.com/sopelj)
9
9
  [![License](https://img.shields.io/github/license/sopelj/python-ember-mug.svg)](LICENSE)
10
10
  [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen)](https://github.com/pre-commit/pre-commit)
@@ -21,16 +21,17 @@ This is an *unofficial* library to attempt to interact with Ember Mugs via Bluet
21
21
  This was created for use with my [Home Assistant integration](https://github.com/sopelj/hass-ember-mug-component),
22
22
  but could be useful separately and has a simple CLI interface too.
23
23
 
24
- All known Ember Mugs, Cups and Travel Mugs have been tested and work.
25
- If I missed one, or you have new feature ideas/issues, please let me know.
24
+ All known Ember Mugs, Cups, Tumblers and Travel Mugs have been tested and seem to work well.
25
+ If I missed one, or you have new feature ideas or issues, please [create an issue](https://github.com/sopelj/python-ember-mug/issues), if it isn't already there, and we'll figure it out.
26
26
 
27
- | Device | Tested |
28
- |--------------|---------|
29
- | Mug | ✓ |
30
- | Mug 2 | ✓ |
31
- | Cup | ✓ |
32
- | Travel Mug | ✓ |
33
- | Travel Mug 2 | ✓ |
27
+ | Device | Tested |
28
+ |--------------|--------|
29
+ | Mug | ✓ |
30
+ | Mug 2 | ✓ |
31
+ | Cup | ✓ |
32
+ | Tumbler | ✓ |
33
+ | Travel Mug | ✓ |
34
+ | Travel Mug 2 | ✓ |
34
35
 
35
36
  ## Features
36
37
 
@@ -41,17 +42,17 @@ If I missed one, or you have new feature ideas/issues, please let me know.
41
42
 
42
43
  Attributes by device:
43
44
 
44
- | Attribute | Mug | Cup | Travel Mug | Description |
45
- |---------------------|-----|-----|------------|-----------------------------------------------|
46
- | Name | R/W | N/A | R | Name to give device |
47
- | LED Colour | R/W | R/W | N/A | Colour of front LED |
48
- | Current Temperature | R | R | R | Current temperature of the liquid in the mug |
49
- | Target Temperature | R/W | R/W | R/W | Desired temperature for the liquid |
50
- | Temperature Unit | R/W | R/W | R/W | Internal temperature unit for the app (C/F) |
51
- | Liquid Level | R | R | R | Approximate level of the liquid in the device |
52
- | Volume level | N/A | N/A | R/W | Volume of the button press beep |
53
- | Battery Percent | R | R | R | Current battery level |
54
- | On Charger | R | R | R | Device is on it's charger |
45
+ | Attribute | Mug | Cup | Tumbler | Travel Mug | Description |
46
+ |---------------------|-----|-----|---------|------------|-----------------------------------------------|
47
+ | Name | R/W | N/A | N/A | R | Name to give device |
48
+ | LED Colour | R/W | R/W | R/W | N/A | Colour of front LED |
49
+ | Current Temperature | R | R | R | R | Current temperature of the liquid in the mug |
50
+ | Target Temperature | R/W | R/W | R/W | R/W | Desired temperature for the liquid |
51
+ | Temperature Unit | R/W | R/W | R/W | R/W | Internal temperature unit for the app (C/F) |
52
+ | Liquid Level | R | R | R | R | Approximate level of the liquid in the device |
53
+ | Volume level | N/A | N/A | N/A | R/W | Volume of the button press beep |
54
+ | Battery Percent | R | R | R | R | Current battery level |
55
+ | On Charger | R | R | R | R | Device is on it's charger |
55
56
 
56
57
  *** Writing may only work if the devices has been set up in the app previously
57
58
 
@@ -115,6 +116,12 @@ Basic options:
115
116
 
116
117
  ## Troubleshooting
117
118
 
119
+ ##### Systematic timeouts or `le-connection-abort-by-local`
120
+
121
+ If your mug gets stuck in a state where it refuses to connect, you get constant reconnects, timeouts, and/or `le-connection-abort-by-local` messages in the debug logs, you may need to remove
122
+ your mug via `bluetoothctl remove my-mac-address` and factory reset your device. It should reconnect correctly afterward.
123
+ You may also need to re-add it to the app in order to make it writable again as well.
124
+
118
125
  ### 'Operation failed with ATT error: 0x0e' or another connection error
119
126
 
120
127
  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.
@@ -5,4 +5,4 @@ __all__ = ("EmberMug",)
5
5
 
6
6
  __author__ = """Jesse Sopel"""
7
7
  __email__ = "jesse.sopel@gmail.com"
8
- __version__ = "0.8.1"
8
+ __version__ = "0.9.0"
@@ -7,18 +7,22 @@ import logging
7
7
  import re
8
8
  import sys
9
9
  from argparse import ArgumentParser, ArgumentTypeError, FileType, Namespace
10
- from typing import TYPE_CHECKING
10
+ from typing import TYPE_CHECKING, ClassVar
11
11
 
12
- from bleak import BleakError
12
+ from bleak import AdvertisementData, BleakError
13
13
 
14
14
  from ember_mug.consts import ATTR_LABELS, EXTRA_ATTRS, IS_LINUX, VolumeLevel
15
15
  from ember_mug.data import Colour
16
16
  from ember_mug.mug import EmberMug
17
17
  from ember_mug.scanner import discover_mugs, find_mug
18
18
 
19
+ from ..formatting import format_capacity
20
+ from ..utils import get_model_info_from_advertiser_data
19
21
  from .helpers import CommandLoop, print_changes, print_info, print_table, validate_mac
20
22
 
21
23
  if TYPE_CHECKING:
24
+ from collections.abc import Awaitable, Callable
25
+
22
26
  from bleak.backends.device import BLEDevice
23
27
 
24
28
  all_attrs = list(ATTR_LABELS) + list(EXTRA_ATTRS)
@@ -27,29 +31,34 @@ get_attribute_names = [n.replace("_", "-") for n in all_attrs]
27
31
 
28
32
  async def get_mug(args: Namespace) -> EmberMug:
29
33
  """Help to get the mug based on args."""
30
- device = await find_device(args)
31
- mug = EmberMug(device, use_metric=not args.imperial, include_extra=args.extra, debug=args.debug)
34
+ device, advertisement = await find_device(args)
35
+ mug = EmberMug(
36
+ device,
37
+ get_model_info_from_advertiser_data(advertisement),
38
+ use_metric=not args.imperial,
39
+ debug=args.debug,
40
+ )
32
41
  if not args.raw:
33
42
  print("Connecting...")
34
43
  return mug
35
44
 
36
45
 
37
- async def find_device(args: Namespace) -> BLEDevice:
46
+ async def find_device(args: Namespace) -> tuple[BLEDevice, AdvertisementData]:
38
47
  """Find a single device that has already been paired."""
39
48
  try:
40
- device = await find_mug(mac=args.mac, adapter=args.adapter)
49
+ device, advertisement = await find_mug(mac=args.mac, adapter=args.adapter)
41
50
  except BleakError as e:
42
51
  print(f"An error occurred trying to find a mug: {e}")
43
52
  sys.exit(1)
44
- if not device:
53
+ if not device or not advertisement:
45
54
  print("No mug was found.")
46
55
  sys.exit(1)
47
56
  if not args.raw:
48
57
  print("Found mug:", device)
49
- return device
58
+ return device, advertisement
50
59
 
51
60
 
52
- async def discover(args: Namespace) -> list[BLEDevice]:
61
+ async def discover(args: Namespace) -> list[tuple[BLEDevice, AdvertisementData]]:
53
62
  """Discover new devices in pairing mode."""
54
63
  try:
55
64
  mugs = await discover_mugs(mac=args.mac)
@@ -60,11 +69,17 @@ async def discover(args: Namespace) -> list[BLEDevice]:
60
69
  print('No mugs were found. Be sure it is in pairing mode. Or use "find" if already paired.')
61
70
  sys.exit(1)
62
71
 
63
- for mug in mugs:
72
+ for mug, advertisement in mugs:
64
73
  if args.raw:
65
74
  print(mug.address)
66
75
  else:
76
+ model_info = get_model_info_from_advertiser_data(advertisement)
77
+ model_number = model_info.model.value if model_info.model else "Unknown Model"
67
78
  print("Found mug:", mug)
79
+ print("Name:", advertisement.local_name)
80
+ print("Model:", f"{model_info.name} [{model_number}]")
81
+ print("Colour:", model_info.colour.value if model_info.colour else "Unknown")
82
+ print("Capacity:", format_capacity(model_info.capacity))
68
83
  return mugs
69
84
 
70
85
 
@@ -119,10 +134,10 @@ async def get_mug_value(args: Namespace) -> None:
119
134
  async def set_mug_value(args: Namespace) -> None:
120
135
  """Set one or more values on the mug."""
121
136
  attrs = ("name", "target_temp", "temperature_unit", "led_colour", "volume_level")
122
- values = [(attr, value) for attr in attrs if (value := getattr(args, attr))]
137
+ values = [(attr, value) for attr in attrs if (value := getattr(args, attr, None))]
123
138
  if not values:
124
139
  print("Please specify at least one attribute and value to set.")
125
- options = [f"--{a}" for a in attrs]
140
+ options = [f"--{a.replace('_', '-')}" for a in attrs]
126
141
  print(f'Options: {", ".join(options)}')
127
142
  sys.exit(1)
128
143
 
@@ -141,15 +156,19 @@ async def set_mug_value(args: Namespace) -> None:
141
156
  def colour_type(value: str) -> Colour:
142
157
  """Convert a hex or rgb colour to a Colour object."""
143
158
  print(value)
144
- if match := re.match(r"#?([0-9a-f]{6})", value, re.IGNORECASE):
145
- colour = match.group(1)
146
- return Colour(*tuple(int(colour[i : i + 2], 16) for i in (0, 2, 4)))
159
+ if match := re.match(r"#?([0-9a-f]{6}([0-9a-f]{2})?)", value, re.IGNORECASE):
160
+ raw_colours = match.group(1)
161
+ colours = [
162
+ 255 if (colour := raw_colours[i : i + 2]) is None else int(colour, 16)
163
+ for i in range(0, len(raw_colours), 2)
164
+ ]
165
+ return Colour(*colours)
147
166
 
148
167
  with contextlib.suppress(ValueError, AssertionError):
149
168
  colours = [int(v) for v in value.split(",")]
150
- if 3 <= len(colours) <= 4:
169
+ if len(colours) not in (3, 4):
151
170
  raise ArgumentTypeError("Three or four values should be specified for colour")
152
- if all(0 <= c <= 255 for c in colours):
171
+ if not all(0 <= c <= 255 for c in colours):
153
172
  raise ArgumentTypeError("Colour values must be between 0 and 255")
154
173
  return Colour(*colours)
155
174
 
@@ -160,7 +179,7 @@ def colour_type(value: str) -> Colour:
160
179
  class EmberMugCli:
161
180
  """Very simple CLI Interface to interact with a mug."""
162
181
 
163
- _commands = {
182
+ _commands: ClassVar[dict[str, Callable[[Namespace], Awaitable]]] = {
164
183
  "find": find_device,
165
184
  "discover": discover,
166
185
  "info": fetch_info,
@@ -58,8 +58,8 @@ def print_table(data: list[tuple[str, ...]]) -> None:
58
58
 
59
59
  def print_info(mug: EmberMug) -> None:
60
60
  """Print all mug data."""
61
- print("Mug Data")
62
- print_table([(k, v) for (k, v) in mug.data.formatted.items()])
61
+ print("Device Data")
62
+ print_table(list(mug.data.formatted.items()))
63
63
 
64
64
 
65
65
  def print_changes(changes: list[Change], metric: bool = True) -> None:
@@ -8,26 +8,62 @@ from functools import cached_property
8
8
  from typing import Literal
9
9
  from uuid import UUID
10
10
 
11
- # Bluetooth names of Ember devices
12
- EMBER_MUG = "Ember Ceramic Mug"
13
- EMBER_CUP = "Ember Cup"
14
- EMBER_CUP_2 = "Ember Cup 2"
15
- EMBER_TRAVEL_MUG_SHORT = "Ember Travel M"
16
- EMBER_TRAVEL_MUG = "Ember Travel Mug"
17
- EMBER_TRAVEL_MUG_2 = "Ember Travel Mug 2"
18
-
19
- EMBER_BLUETOOTH_NAMES: tuple[str, ...] = (
20
- EMBER_MUG,
21
- EMBER_CUP,
22
- EMBER_CUP_2,
23
- EMBER_TRAVEL_MUG,
24
- EMBER_TRAVEL_MUG_SHORT,
25
- EMBER_TRAVEL_MUG_2,
26
- )
27
-
28
11
  # Format for all the mug's Bluetooth UUIDs
29
12
  UUID_TEMPLATE = "fc54{:0>4x}-236c-4c94-8fa9-944a3e5353fa"
30
13
 
14
+ # Registered SIG for BLE Manufacturer Data
15
+ EMBER_BLE_SIG = 0x03C1
16
+ DEFAULT_NAME = "Ember Device"
17
+
18
+
19
+ class DeviceType(str, Enum):
20
+ """Base device types."""
21
+
22
+ CUP = "cup"
23
+ MUG = "mug"
24
+ TRAVEL_MUG = "travel_mug"
25
+ TUMBLER = "tumbler"
26
+
27
+
28
+ class DeviceModel(str, Enum):
29
+ """Know device models."""
30
+
31
+ CUP_6_OZ = "CM21S"
32
+ MUG_1_10_OZ = "CM17"
33
+ MUG_1_14_OZ = "CM17P"
34
+ MUG_2_10_OZ = "CM19/CM21M"
35
+ MUG_2_14_OZ = "CM19P/CM21L"
36
+ TRAVEL_MUG_12_OZ = "TM19"
37
+ TUMBLER_16_OZ = "CM21XL"
38
+ UNKNOWN_DEVICE = "Unknown"
39
+
40
+
41
+ DEVICE_MODEL_NAMES: dict[DeviceModel, str] = {
42
+ DeviceModel.CUP_6_OZ: "Ember Cup",
43
+ DeviceModel.MUG_1_10_OZ: "Ember Mug (10oz)",
44
+ DeviceModel.MUG_1_14_OZ: "Ember Mug (14oz)",
45
+ DeviceModel.MUG_2_10_OZ: "Ember Mug 2 (10oz)",
46
+ DeviceModel.MUG_2_14_OZ: "Ember Mug 2 (14oz)",
47
+ DeviceModel.TRAVEL_MUG_12_OZ: "Ember Travel Mug",
48
+ DeviceModel.TUMBLER_16_OZ: "Ember Tumbler",
49
+ }
50
+
51
+
52
+ class DeviceColour(str, Enum):
53
+ """All colours possible found across models."""
54
+
55
+ SAGE_GREEN = "Sage Green"
56
+ SANDSTONE = "Sandstone"
57
+ BLACK = "Black"
58
+ WHITE = "White"
59
+ GREY = "Grey"
60
+ BLUE = "Blue"
61
+ RED = "Red"
62
+ COPPER = "Copper"
63
+ GOLD = "Gold"
64
+ STAINLESS_STEEL = "Stainless Steel"
65
+ ROSE_GOLD = "Rose Gold"
66
+
31
67
 
32
68
  class TemperatureUnit(str, Enum):
33
69
  """Temperature Units."""
@@ -80,8 +116,9 @@ class MugCharacteristic(IntEnum):
80
116
  # RGBA Colour of LED (Read/Write)
81
117
  LED = 20
82
118
  # Service
83
- TRAVEL_MUG_SERVICE = 13857
84
119
  STANDARD_SERVICE = 13858
120
+ TRAVEL_MUG_SERVICE = 13857
121
+ TRAVEL_MUG_SERVICE_OTHER = 8609
85
122
 
86
123
  @cached_property
87
124
  def uuid(self) -> UUID:
@@ -93,10 +130,21 @@ class MugCharacteristic(IntEnum):
93
130
  return str(self.uuid)
94
131
 
95
132
 
133
+ TRAVEL_MUG_SERVICE_UUIDS = (
134
+ str(MugCharacteristic.TRAVEL_MUG_SERVICE),
135
+ str(MugCharacteristic.TRAVEL_MUG_SERVICE_OTHER),
136
+ )
137
+
138
+ DEVICE_SERVICE_UUIDS = (
139
+ str(MugCharacteristic.STANDARD_SERVICE),
140
+ *TRAVEL_MUG_SERVICE_UUIDS,
141
+ )
142
+
143
+
96
144
  class LiquidState(IntEnum):
97
145
  """Constants for liquid state codes."""
98
146
 
99
- UNKNOWN = 0
147
+ STANDBY = 0
100
148
  EMPTY = 1
101
149
  FILLING = 2
102
150
  COLD_NO_TEMP_CONTROL = 3
@@ -149,8 +197,9 @@ class PushEvent(IntEnum):
149
197
 
150
198
 
151
199
  # Labels so liquid states
200
+ LIQUID_STATE_UNKNOWN = "Unknown"
152
201
  LIQUID_STATE_LABELS: dict[int, str] = {
153
- LiquidState.UNKNOWN: "Unknown",
202
+ LiquidState.STANDBY: "Standby",
154
203
  LiquidState.EMPTY: "Empty",
155
204
  LiquidState.FILLING: "Filling",
156
205
  LiquidState.COLD_NO_TEMP_CONTROL: "Cold (No control)",
@@ -168,7 +217,7 @@ PUSH_EVENT_BATTERY_IDS = [
168
217
 
169
218
  # Labels for formatting attributes
170
219
  ATTR_LABELS = {
171
- "name": "Mug Name",
220
+ "name": "Device Name",
172
221
  "meta": "Meta",
173
222
  "battery": "Battery",
174
223
  "firmware": "Firmware",
@@ -203,7 +252,7 @@ UPDATE_ATTRS = {
203
252
  "liquid_level",
204
253
  "liquid_state",
205
254
  }
206
- EXTRA_ATTRS = {"dsk", "udsk", "battery_voltage", "date_time_zone"}
255
+ EXTRA_ATTRS = {"battery_voltage", "date_time_zone", "udsk", "dsk"}
207
256
 
208
257
  # Validation
209
258
  MUG_NAME_REGEX = re.compile(r"^[A-Za-z0-9,.\[\]#()!\"\';:|\-_+<>%= ]{1,16}$")