Skip to content

data.py

data

Classes for representing data from the mug.

Classes

AsDict

Mixin to add as_dict to dataclass for serialization.

Source code in ember_mug/data.py
64
65
66
67
68
69
class AsDict:
    """Mixin to add as_dict to dataclass for serialization."""

    def as_dict(self: DataclassInstance) -> dict[str, Any]:
        """Add as_dict to dataclass for serialization."""
        return asdict(self)
Functions
as_dict
as_dict() -> dict[str, Any]

Add as_dict to dataclass for serialization.

Source code in ember_mug/data.py
67
68
69
def as_dict(self: DataclassInstance) -> dict[str, Any]:
    """Add as_dict to dataclass for serialization."""
    return asdict(self)

BaseModelInfo dataclass

Bases: AsDict

Base class to declare properties as field.

Source code in ember_mug/data.py
120
121
122
123
124
125
126
127
128
@dataclass
class BaseModelInfo(AsDict):
    """Base class to declare properties as field."""

    model: DeviceModel | None = None
    colour: DeviceColour | None = None
    name: str = field(init=False)
    capacity: int | None = field(init=False)
    device_type: DeviceType = field(init=False)
Attributes
capacity class-attribute instance-attribute
capacity: int | None = field(init=False)
colour class-attribute instance-attribute
colour: DeviceColour | None = None
device_type class-attribute instance-attribute
device_type: DeviceType = field(init=False)
model class-attribute instance-attribute
model: DeviceModel | None = None
name class-attribute instance-attribute
name: str = field(init=False)
Functions
as_dict
as_dict() -> dict[str, Any]

Add as_dict to dataclass for serialization.

Source code in ember_mug/data.py
67
68
69
def as_dict(self: DataclassInstance) -> dict[str, Any]:
    """Add as_dict to dataclass for serialization."""
    return asdict(self)

BatteryInfo dataclass

Bases: AsDict

Battery Information.

Source code in ember_mug/data.py
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
@dataclass
class BatteryInfo(AsDict):
    """Battery Information."""

    percent: float
    on_charging_base: bool

    @classmethod
    def from_bytes(cls, data: bytes) -> BatteryInfo:
        """Initialize from raw bytes."""
        return cls(
            percent=round(float(data[0]), 2),
            on_charging_base=data[1] == 1,
        )

    def __str__(self) -> str:
        """Format nicely for printing."""
        return f'{self.percent}%, {"" if self.on_charging_base else "not "}on charging base'
Attributes
on_charging_base instance-attribute
on_charging_base: bool
percent instance-attribute
percent: float
Functions
as_dict
as_dict() -> dict[str, Any]

Add as_dict to dataclass for serialization.

Source code in ember_mug/data.py
67
68
69
def as_dict(self: DataclassInstance) -> dict[str, Any]:
    """Add as_dict to dataclass for serialization."""
    return asdict(self)
from_bytes classmethod
from_bytes(data: bytes) -> BatteryInfo

Initialize from raw bytes.

Source code in ember_mug/data.py
79
80
81
82
83
84
85
@classmethod
def from_bytes(cls, data: bytes) -> BatteryInfo:
    """Initialize from raw bytes."""
    return cls(
        percent=round(float(data[0]), 2),
        on_charging_base=data[1] == 1,
    )

Change

Bases: NamedTuple

Helper for storing changes to attributes.

Source code in ember_mug/data.py
31
32
33
34
35
36
37
38
39
40
class Change(NamedTuple):
    """Helper for storing changes to attributes."""

    attr: str
    old_value: Any
    new_value: Any

    def __str__(self) -> str:
        """Use str to format Change message."""
        return f'{self.attr.replace("_", " ").title()} changed from "{self.old_value}" to "{self.new_value}"'
Attributes
attr instance-attribute
attr: str
new_value instance-attribute
new_value: Any
old_value instance-attribute
old_value: Any

Colour

Bases: NamedTuple

Simple helper for colour formatting.

Source code in ember_mug/data.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Colour(NamedTuple):
    """Simple helper for colour formatting."""

    red: int
    green: int
    blue: int
    brightness: int = 255

    def as_hex(self) -> str:
        """Format colour array as hex string."""
        return "#" + "".join(f"{c:02x}" for c in self)[:6]

    def as_bytearray(self) -> bytearray:
        """Convert to byte array."""
        return bytearray(c for c in self)

    def __str__(self) -> str:
        """For more useful cli output, format as hex."""
        return self.as_hex()
Attributes
blue instance-attribute
blue: int
brightness class-attribute instance-attribute
brightness: int = 255
green instance-attribute
green: int
red instance-attribute
red: int
Functions
as_bytearray
as_bytearray() -> bytearray

Convert to byte array.

Source code in ember_mug/data.py
55
56
57
def as_bytearray(self) -> bytearray:
    """Convert to byte array."""
    return bytearray(c for c in self)
as_hex
as_hex() -> str

Format colour array as hex string.

Source code in ember_mug/data.py
51
52
53
def as_hex(self) -> str:
    """Format colour array as hex string."""
    return "#" + "".join(f"{c:02x}" for c in self)[:6]

ModelInfo dataclass

Bases: BaseModelInfo

Model name and attributes based on mode.

Source code in ember_mug/data.py
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
@dataclass
class ModelInfo(BaseModelInfo):
    """Model name and attributes based on mode."""

    @cached_property  # type: ignore[misc]
    def name(self) -> str:  # type: ignore[override]
        """Get a human-readable name from model number."""
        return DEVICE_MODEL_NAMES.get(
            self.model or DeviceModel.UNKNOWN_DEVICE,
            "Unknown Device",
        )

    @cached_property  # type: ignore[misc]
    def capacity(self) -> int | None:  # type: ignore[override]
        """Determine capacity in mL based on model number."""
        if self.model == DeviceModel.CUP_6_OZ:
            return 178  # ml - 6oz
        if self.model in (DeviceModel.MUG_1_10_OZ, DeviceModel.MUG_2_10_OZ):
            return 295  # ml - 10oz
        if self.model == DeviceModel.TRAVEL_MUG_12_OZ:
            return 355  # ml - 12oz
        if self.model in (DeviceModel.MUG_1_14_OZ, DeviceModel.MUG_2_14_OZ):
            return 414  # ml - 14oz
        if self.model == DeviceModel.TUMBLER_16_OZ:
            return 473  # ml - 16oz
        return None

    @cached_property  # type: ignore[misc]
    def device_type(self) -> DeviceType:  # type: ignore[override]
        """Basic device type from model number."""
        if self.model == DeviceModel.TRAVEL_MUG_12_OZ:
            return DeviceType.TRAVEL_MUG
        if self.model == DeviceModel.TUMBLER_16_OZ:
            return DeviceType.TUMBLER
        if self.model == DeviceModel.CUP_6_OZ:
            return DeviceType.CUP
        # This could be an unknown device, but fallback to mug
        return DeviceType.MUG

    @cached_property
    def device_attributes(self) -> set[str]:
        """Attributes to update based on model and extra."""
        attributes = EXTRA_ATTRS | INITIAL_ATTRS | UPDATE_ATTRS
        unknown = (None, DeviceModel.UNKNOWN_DEVICE)
        if self.model in unknown or self.device_type in (DeviceType.CUP, DeviceType.TUMBLER):
            # The Cup and Tumbler cannot be named
            attributes -= {"name"}
        elif self.model in unknown or self.device_type == DeviceType.TRAVEL_MUG:
            # Tge Travel Mug does not have an LED colour, but has a volume attribute
            attributes = (attributes - {"led_colour"}) | {"volume_level"}
        if self.model != DeviceModel.TRAVEL_MUG_12_OZ:
            # Only Travel mug has this attribute?
            attributes -= {"battery_voltage"}
        return attributes
Attributes
capacity cached property
capacity: int | None

Determine capacity in mL based on model number.

colour class-attribute instance-attribute
colour: DeviceColour | None = None
device_attributes cached property
device_attributes: set[str]

Attributes to update based on model and extra.

device_type cached property
device_type: DeviceType

Basic device type from model number.

model class-attribute instance-attribute
model: DeviceModel | None = None
name cached property
name: str

Get a human-readable name from model number.

Functions
as_dict
as_dict() -> dict[str, Any]

Add as_dict to dataclass for serialization.

Source code in ember_mug/data.py
67
68
69
def as_dict(self: DataclassInstance) -> dict[str, Any]:
    """Add as_dict to dataclass for serialization."""
    return asdict(self)

MugData dataclass

Bases: AsDict

Class to store/display the state of the mug.

Source code in ember_mug/data.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
@dataclass
class MugData(AsDict):
    """Class to store/display the state of the mug."""

    # Options
    model_info: ModelInfo
    use_metric: bool = True
    debug: bool = False

    # Attributes
    name: str = ""
    meta: MugMeta | None = None
    battery: BatteryInfo | None = None
    firmware: MugFirmwareInfo | None = None
    led_colour: Colour = field(default_factory=lambda: Colour(255, 255, 255, 255))
    liquid_state: LiquidState | None = None
    liquid_level: int = 0
    temperature_unit: TemperatureUnit = TemperatureUnit.CELSIUS
    current_temp: float = 0.0
    target_temp: float = 0.0
    dsk: str = ""
    udsk: str | None = ""
    volume_level: VolumeLevel | None = None
    date_time_zone: datetime | None = None
    battery_voltage: int | None = None

    @property
    def meta_display(self) -> str:
        """Return Meta infor based on preference."""
        if self.meta and not self.debug:
            return f"Serial Number: {self.meta.serial_number}"
        return str(self.meta)

    @property
    def led_colour_display(self) -> str:
        """Return colour as hex value."""
        return format_led_colour(self.led_colour)

    @property
    def liquid_state_display(self) -> str:
        """Human-readable liquid state."""
        return self.liquid_state.label if self.liquid_state else LIQUID_STATE_UNKNOWN

    @property
    def volume_level_display(self) -> str | None:
        """Human-readable volume level."""
        if self.volume_level:
            return self.volume_level.value.capitalize()
        return None

    @property
    def liquid_level_display(self) -> str:
        """Human-readable liquid level."""
        return format_liquid_level(self.liquid_level)

    @property
    def current_temp_display(self) -> str:
        """Human-readable current temp with unit."""
        return format_temp(self.current_temp, self.use_metric)

    @property
    def target_temp_display(self) -> str:
        """Human-readable target temp with unit."""
        return format_temp(self.target_temp, self.use_metric)

    def update_info(self, **kwargs: Any) -> list[Change]:
        """Update attributes of the mug if they haven't changed."""
        changes: list[Change] = []
        for attr, new_value in kwargs.items():
            if (old_value := getattr(self, attr)) != new_value:
                setattr(self, attr, new_value)
                changes.append(Change(attr, old_value, new_value))
        return changes

    def get_formatted_attr(self, attr: str) -> str | None:
        """Get the display value of a given attribute."""
        if display_value := getattr(self, f"{attr}_display", None):
            return display_value
        return getattr(self, attr)

    @property
    def formatted(self) -> dict[str, Any]:
        """Return human-readable names and values for all attributes for display."""
        all_attrs = self.model_info.device_attributes | {"use_metric"}
        if not self.debug:
            all_attrs -= EXTRA_ATTRS
        return {label: self.get_formatted_attr(attr) for attr, label in ATTR_LABELS.items() if attr in all_attrs}

    def as_dict(self) -> dict[str, Any]:
        """Dump all attributes as dict for info/debugging."""
        data = asdict(self)
        all_attrs = self.model_info.device_attributes
        if not self.debug:
            all_attrs -= EXTRA_ATTRS
        data.update(
            {
                f"{attr}_display": getattr(self, f"{attr}_display", None)
                for attr in all_attrs
                if hasattr(self, f"{attr}_display")
            },
        )
        return data
Attributes
battery class-attribute instance-attribute
battery: BatteryInfo | None = None
battery_voltage class-attribute instance-attribute
battery_voltage: int | None = None
current_temp class-attribute instance-attribute
current_temp: float = 0.0
current_temp_display property
current_temp_display: str

Human-readable current temp with unit.

date_time_zone class-attribute instance-attribute
date_time_zone: datetime | None = None
debug class-attribute instance-attribute
debug: bool = False
dsk class-attribute instance-attribute
dsk: str = ''
firmware class-attribute instance-attribute
firmware: MugFirmwareInfo | None = None
formatted property
formatted: dict[str, Any]

Return human-readable names and values for all attributes for display.

led_colour class-attribute instance-attribute
led_colour: Colour = field(
    default_factory=lambda: Colour(255, 255, 255, 255)
)
led_colour_display property
led_colour_display: str

Return colour as hex value.

liquid_level class-attribute instance-attribute
liquid_level: int = 0
liquid_level_display property
liquid_level_display: str

Human-readable liquid level.

liquid_state class-attribute instance-attribute
liquid_state: LiquidState | None = None
liquid_state_display property
liquid_state_display: str

Human-readable liquid state.

meta class-attribute instance-attribute
meta: MugMeta | None = None
meta_display property
meta_display: str

Return Meta infor based on preference.

model_info instance-attribute
model_info: ModelInfo
name class-attribute instance-attribute
name: str = ''
target_temp class-attribute instance-attribute
target_temp: float = 0.0
target_temp_display property
target_temp_display: str

Human-readable target temp with unit.

temperature_unit class-attribute instance-attribute
temperature_unit: TemperatureUnit = CELSIUS
udsk class-attribute instance-attribute
udsk: str | None = ''
use_metric class-attribute instance-attribute
use_metric: bool = True
volume_level class-attribute instance-attribute
volume_level: VolumeLevel | None = None
volume_level_display property
volume_level_display: str | None

Human-readable volume level.

Functions
as_dict
as_dict() -> dict[str, Any]

Dump all attributes as dict for info/debugging.

Source code in ember_mug/data.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
def as_dict(self) -> dict[str, Any]:
    """Dump all attributes as dict for info/debugging."""
    data = asdict(self)
    all_attrs = self.model_info.device_attributes
    if not self.debug:
        all_attrs -= EXTRA_ATTRS
    data.update(
        {
            f"{attr}_display": getattr(self, f"{attr}_display", None)
            for attr in all_attrs
            if hasattr(self, f"{attr}_display")
        },
    )
    return data
get_formatted_attr
get_formatted_attr(attr: str) -> str | None

Get the display value of a given attribute.

Source code in ember_mug/data.py
281
282
283
284
285
def get_formatted_attr(self, attr: str) -> str | None:
    """Get the display value of a given attribute."""
    if display_value := getattr(self, f"{attr}_display", None):
        return display_value
    return getattr(self, attr)
update_info
update_info(**kwargs: Any) -> list[Change]

Update attributes of the mug if they haven't changed.

Source code in ember_mug/data.py
272
273
274
275
276
277
278
279
def update_info(self, **kwargs: Any) -> list[Change]:
    """Update attributes of the mug if they haven't changed."""
    changes: list[Change] = []
    for attr, new_value in kwargs.items():
        if (old_value := getattr(self, attr)) != new_value:
            setattr(self, attr, new_value)
            changes.append(Change(attr, old_value, new_value))
    return changes

MugFirmwareInfo dataclass

Bases: AsDict

Firmware versions.

Source code in ember_mug/data.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
@dataclass
class MugFirmwareInfo(AsDict):
    """Firmware versions."""

    version: int
    hardware: int
    bootloader: int

    @classmethod
    def from_bytes(cls, data: bytes) -> MugFirmwareInfo:
        """Initialize from raw bytes."""
        return cls(
            version=bytes_to_little_int(data[:2]),
            hardware=bytes_to_little_int(data[2:4]),
            bootloader=bytes_to_little_int(data[4:]),
        )

    def __str__(self) -> str:
        """Format nicely for printing."""
        return ", ".join(
            (
                f"Version: {self.version}",
                f"Hardware: {self.hardware}",
                f"Bootloader: {self.bootloader}",
            ),
        )
Attributes
bootloader instance-attribute
bootloader: int
hardware instance-attribute
hardware: int
version instance-attribute
version: int
Functions
as_dict
as_dict() -> dict[str, Any]

Add as_dict to dataclass for serialization.

Source code in ember_mug/data.py
67
68
69
def as_dict(self: DataclassInstance) -> dict[str, Any]:
    """Add as_dict to dataclass for serialization."""
    return asdict(self)
from_bytes classmethod
from_bytes(data: bytes) -> MugFirmwareInfo

Initialize from raw bytes.

Source code in ember_mug/data.py
100
101
102
103
104
105
106
107
@classmethod
def from_bytes(cls, data: bytes) -> MugFirmwareInfo:
    """Initialize from raw bytes."""
    return cls(
        version=bytes_to_little_int(data[:2]),
        hardware=bytes_to_little_int(data[2:4]),
        bootloader=bytes_to_little_int(data[4:]),
    )

MugMeta dataclass

Bases: AsDict

Meta data for mug.

Source code in ember_mug/data.py
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
@dataclass
class MugMeta(AsDict):
    """Meta data for mug."""

    mug_id: str  # unsure if this value is properly decoded
    serial_number: str

    @classmethod
    def from_bytes(cls, data: bytes) -> MugMeta:
        """Initialize from raw bytes."""
        return cls(
            mug_id=decode_byte_string(data[:6]),
            serial_number=data[7:].decode(),
        )

    def __str__(self) -> str:
        """Format nicely for printing."""
        return f"Mug ID: {self.mug_id}, Serial Number: {self.serial_number}"
Attributes
mug_id instance-attribute
mug_id: str
serial_number instance-attribute
serial_number: str
Functions
as_dict
as_dict() -> dict[str, Any]

Add as_dict to dataclass for serialization.

Source code in ember_mug/data.py
67
68
69
def as_dict(self: DataclassInstance) -> dict[str, Any]:
    """Add as_dict to dataclass for serialization."""
    return asdict(self)
from_bytes classmethod
from_bytes(data: bytes) -> MugMeta

Initialize from raw bytes.

Source code in ember_mug/data.py
194
195
196
197
198
199
200
@classmethod
def from_bytes(cls, data: bytes) -> MugMeta:
    """Initialize from raw bytes."""
    return cls(
        mug_id=decode_byte_string(data[:6]),
        serial_number=data[7:].decode(),
    )