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.
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/PKG-INFO +33 -24
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/README.md +28 -21
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/__init__.py +1 -1
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/cli/commands.py +37 -18
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/cli/helpers.py +2 -2
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/consts.py +71 -22
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/data.py +72 -63
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/formatting.py +11 -0
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/mug.py +71 -42
- python_ember_mug-0.9.0/ember_mug/scanner.py +68 -0
- python_ember_mug-0.9.0/ember_mug/utils.py +195 -0
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/pyproject.toml +13 -6
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/cli/test_commands.py +105 -17
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/cli/test_helpers.py +3 -2
- python_ember_mug-0.9.0/tests/conftest.py +92 -0
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_connection.py +165 -131
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_consts.py +1 -1
- python_ember_mug-0.9.0/tests/test_data.py +78 -0
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_formatting.py +7 -1
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_mug_data.py +28 -14
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/test_scanner.py +25 -23
- python_ember_mug-0.9.0/tests/test_utils.py +179 -0
- python_ember_mug-0.8.1/ember_mug/scanner.py +0 -60
- python_ember_mug-0.8.1/ember_mug/utils.py +0 -100
- python_ember_mug-0.8.1/tests/conftest.py +0 -47
- python_ember_mug-0.8.1/tests/test_data.py +0 -75
- python_ember_mug-0.8.1/tests/test_utils.py +0 -70
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/.gitignore +0 -0
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/LICENSE +0 -0
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/__main__.py +0 -0
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/ember_mug/cli/__init__.py +0 -0
- {python_ember_mug-0.8.1 → python_ember_mug-0.9.0}/tests/__init__.py +0 -0
- {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.
|
|
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:
|
|
20
|
-
Requires-Dist: bleak>=
|
|
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
|
[](https://pypi.org/project/python-ember-mug/)
|
|
38
40
|
[](https://github.com/sopelj/python-ember-mug/actions/workflows/tests.yml)
|
|
39
41
|
[](https://codecov.io/github/sopelj/python-ember-mug)
|
|
40
|
-

|
|
41
43
|
[](https://github.com/sopelj)
|
|
42
44
|
[](LICENSE)
|
|
43
45
|
[](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
|
|
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
|
-
|
|
|
66
|
-
| Travel Mug
|
|
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
|
[](https://pypi.org/project/python-ember-mug/)
|
|
5
5
|
[](https://github.com/sopelj/python-ember-mug/actions/workflows/tests.yml)
|
|
6
6
|
[](https://codecov.io/github/sopelj/python-ember-mug)
|
|
7
|
-

|
|
8
8
|
[](https://github.com/sopelj)
|
|
9
9
|
[](LICENSE)
|
|
10
10
|
[](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
|
|
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
|
-
|
|
|
33
|
-
| Travel Mug
|
|
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.
|
|
@@ -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(
|
|
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
|
-
|
|
146
|
-
|
|
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
|
|
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("
|
|
62
|
-
print_table(
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
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 = {"
|
|
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}$")
|