cremalink 0.1.0b5__py3-none-any.whl

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 (47) hide show
  1. cremalink/__init__.py +33 -0
  2. cremalink/clients/__init__.py +10 -0
  3. cremalink/clients/cloud.py +130 -0
  4. cremalink/core/__init__.py +6 -0
  5. cremalink/core/binary.py +102 -0
  6. cremalink/crypto/__init__.py +142 -0
  7. cremalink/devices/AY008ESP1.json +114 -0
  8. cremalink/devices/__init__.py +116 -0
  9. cremalink/domain/__init__.py +11 -0
  10. cremalink/domain/device.py +245 -0
  11. cremalink/domain/factory.py +98 -0
  12. cremalink/local_server.py +76 -0
  13. cremalink/local_server_app/__init__.py +20 -0
  14. cremalink/local_server_app/api.py +272 -0
  15. cremalink/local_server_app/config.py +64 -0
  16. cremalink/local_server_app/device_adapter.py +96 -0
  17. cremalink/local_server_app/jobs.py +104 -0
  18. cremalink/local_server_app/logging.py +116 -0
  19. cremalink/local_server_app/models.py +76 -0
  20. cremalink/local_server_app/protocol.py +135 -0
  21. cremalink/local_server_app/state.py +358 -0
  22. cremalink/parsing/__init__.py +7 -0
  23. cremalink/parsing/monitor/__init__.py +22 -0
  24. cremalink/parsing/monitor/decode.py +79 -0
  25. cremalink/parsing/monitor/extractors.py +69 -0
  26. cremalink/parsing/monitor/frame.py +132 -0
  27. cremalink/parsing/monitor/model.py +42 -0
  28. cremalink/parsing/monitor/profile.py +144 -0
  29. cremalink/parsing/monitor/view.py +196 -0
  30. cremalink/parsing/properties/__init__.py +9 -0
  31. cremalink/parsing/properties/decode.py +53 -0
  32. cremalink/resources/__init__.py +10 -0
  33. cremalink/resources/api_config.json +14 -0
  34. cremalink/resources/api_config.py +30 -0
  35. cremalink/resources/lang.json +223 -0
  36. cremalink/transports/__init__.py +7 -0
  37. cremalink/transports/base.py +94 -0
  38. cremalink/transports/cloud/__init__.py +9 -0
  39. cremalink/transports/cloud/transport.py +166 -0
  40. cremalink/transports/local/__init__.py +9 -0
  41. cremalink/transports/local/transport.py +164 -0
  42. cremalink-0.1.0b5.dist-info/METADATA +138 -0
  43. cremalink-0.1.0b5.dist-info/RECORD +47 -0
  44. cremalink-0.1.0b5.dist-info/WHEEL +5 -0
  45. cremalink-0.1.0b5.dist-info/entry_points.txt +2 -0
  46. cremalink-0.1.0b5.dist-info/licenses/LICENSE +661 -0
  47. cremalink-0.1.0b5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,69 @@
1
+ """
2
+ This module provides extractor functions that pull data from a raw monitor frame.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ import base64
7
+ from typing import Any, Tuple
8
+
9
+ from cremalink.parsing.monitor.frame import MonitorFrame
10
+
11
+
12
+ def extract_fields_from_b64(
13
+ raw_b64: str,
14
+ ) -> Tuple[dict[str, Any], list[str], list[str], MonitorFrame | None]:
15
+ """
16
+ Parses a base64-encoded monitor string into a low-level MonitorFrame
17
+ and extracts its fields into a dictionary.
18
+
19
+ This function serves as the first step in the decoding pipeline. It handles
20
+ the initial, structural parsing of the byte frame and populates a dictionary
21
+ with the raw integer and byte values.
22
+
23
+ Args:
24
+ raw_b64: The base64-encoded monitor data string.
25
+
26
+ Returns:
27
+ A tuple containing:
28
+ - A dictionary of the extracted raw fields.
29
+ - A list of any warnings generated during parsing.
30
+ - A list of any errors encountered during parsing.
31
+ - The parsed `MonitorFrame` object, or None if parsing failed.
32
+ """
33
+ parsed: dict[str, Any] = {}
34
+ warnings: list[str] = []
35
+ errors: list[str] = []
36
+ frame: MonitorFrame | None = None
37
+
38
+ # --- Step 1: Decode the raw bytes into a MonitorFrame ---
39
+ try:
40
+ frame = MonitorFrame.from_b64(raw_b64)
41
+ except Exception as exc:
42
+ # If frame parsing fails, record the error and exit.
43
+ errors.append(f"parse_failed: {exc}")
44
+ try:
45
+ # As a fallback, try to at least record the length of the raw data.
46
+ raw = base64.b64decode(raw_b64)
47
+ parsed["raw_length"] = len(raw)
48
+ except Exception:
49
+ pass
50
+ return parsed, warnings, errors, frame
51
+
52
+ # --- Step 2: Populate the 'parsed' dictionary with the frame's fields ---
53
+ parsed.update(
54
+ {
55
+ "accessory": frame.accessory,
56
+ "switches": list(frame.switches),
57
+ "alarms": list(frame.alarms),
58
+ "status": frame.status,
59
+ "action": frame.action,
60
+ "progress": frame.progress,
61
+ "direction": frame.direction,
62
+ "request_id": frame.request_id,
63
+ "answer_required": frame.answer_required,
64
+ "timestamp": frame.timestamp.hex() if frame.timestamp else "",
65
+ "extra": frame.extra.hex() if frame.extra else "",
66
+ }
67
+ )
68
+
69
+ return parsed, warnings, errors, frame
@@ -0,0 +1,132 @@
1
+ """
2
+ This module defines the low-level structure of a "monitor" data frame.
3
+ It handles the byte-level decoding of the raw payload from the device.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import base64
8
+ from dataclasses import dataclass
9
+
10
+ from cremalink.core.binary import crc16_ccitt
11
+
12
+
13
+ @dataclass
14
+ class MonitorFrame:
15
+ """
16
+ Represents the decoded, low-level structure of a monitor data frame.
17
+
18
+ This class takes the raw bytes from a monitor update and parses them into
19
+ their fundamental components according to the device's binary protocol.
20
+ This includes separating headers, payload, checksums, and timestamps.
21
+ """
22
+ # --- Frame Header/Metadata ---
23
+ direction: int
24
+ request_id: int
25
+ answer_required: int
26
+ # --- Core Payload Fields ---
27
+ accessory: int
28
+ switches: bytes
29
+ alarms: bytes
30
+ status: int
31
+ action: int
32
+ progress: int
33
+ # --- Frame Footer/Extra Data ---
34
+ timestamp: bytes
35
+ extra: bytes
36
+ # --- Raw Data ---
37
+ raw: bytes
38
+ raw_b64: str
39
+
40
+ @classmethod
41
+ def from_b64(cls, raw_b64: str) -> "MonitorFrame":
42
+ """
43
+ Decodes a base64 string into a structured MonitorFrame.
44
+
45
+ This factory method performs the primary byte-level parsing, including:
46
+ - Base64 decoding.
47
+ - Length validation.
48
+ - CRC-16 checksum verification.
49
+ - Splitting the raw bytes into their respective fields (header, payload, etc.).
50
+
51
+ Args:
52
+ raw_b64: The base64-encoded monitor data string.
53
+
54
+ Returns:
55
+ A populated MonitorFrame instance.
56
+
57
+ Raises:
58
+ ValueError: If the data is malformed, too short, or fails the CRC check.
59
+ """
60
+ raw = base64.b64decode(raw_b64)
61
+ if len(raw) < 4:
62
+ raise ValueError("Raw data is too short to contain a monitor frame")
63
+
64
+ # --- Unpack the outer frame ---
65
+ direction = raw[0]
66
+ length = raw[1]
67
+ if length < 4 or len(raw) < length + 1:
68
+ raise ValueError("Length byte inconsistent with payload")
69
+
70
+ data = raw[2: length - 1]
71
+ crc = raw[length - 1: length + 1]
72
+
73
+ # --- Verify CRC ---
74
+ if crc != crc16_ccitt(raw[: length - 1]):
75
+ raise ValueError("CRC check failed")
76
+
77
+ timestamp = raw[length + 1: length + 5]
78
+ extra = raw[length + 5:]
79
+
80
+ # --- Unpack the inner monitor data payload ---
81
+ if len(data) < 2:
82
+ raise ValueError("Monitor payload too short")
83
+ request_id = data[0]
84
+ answer_required = data[1]
85
+ contents = data[2:]
86
+
87
+ # This implementation assumes a "V2" frame structure with 13 bytes of contents.
88
+ if len(contents) != 13:
89
+ raise ValueError("Monitor contents expected to be 13 bytes for V2 frames")
90
+
91
+ accessory = contents[0]
92
+ switches = contents[1:3]
93
+ # Alarms are non-contiguous in the payload, so they are concatenated here.
94
+ alarms = contents[3:5] + contents[8:10]
95
+ status = contents[5]
96
+ action = contents[6]
97
+ progress = contents[7]
98
+
99
+ return cls(
100
+ direction=direction,
101
+ request_id=request_id,
102
+ answer_required=answer_required,
103
+ accessory=accessory,
104
+ switches=switches,
105
+ alarms=alarms,
106
+ status=status,
107
+ action=action,
108
+ progress=progress,
109
+ timestamp=timestamp,
110
+ extra=extra,
111
+ raw=raw,
112
+ raw_b64=raw_b64,
113
+ )
114
+
115
+ def as_dict(self) -> dict:
116
+ """
117
+ Returns a dictionary representation of the frame's contents.
118
+ This is useful for serialization or debugging.
119
+ """
120
+ return {
121
+ "direction": self.direction,
122
+ "request_id": self.request_id,
123
+ "answer_required": self.answer_required,
124
+ "accessory": self.accessory,
125
+ "switches": list(self.switches),
126
+ "alarms": list(self.alarms),
127
+ "status": self.status,
128
+ "action": self.action,
129
+ "progress": self.progress,
130
+ "timestamp": self.timestamp.hex() if self.timestamp else "",
131
+ "extra": self.extra.hex() if self.extra else "",
132
+ }
@@ -0,0 +1,42 @@
1
+ """
2
+ This module defines the high-level data model for a "monitor" data snapshot.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from typing import Any, Optional
9
+
10
+ from cremalink.parsing.monitor.frame import MonitorFrame
11
+
12
+
13
+ @dataclass
14
+ class MonitorSnapshot:
15
+ """
16
+ Represents a single snapshot of the device's monitoring status.
17
+
18
+ This dataclass acts as a container for all information related to a single
19
+ monitor update from the device. It holds the raw data, timestamps, any
20
+ parsed values, and metadata about the decoding process.
21
+
22
+ Attributes:
23
+ raw: The raw bytes of the monitor data payload.
24
+ raw_b64: The base64-encoded string representation of the raw data.
25
+ received_at: The timestamp when this snapshot was received.
26
+ parsed: A dictionary to hold the successfully parsed key-value data.
27
+ warnings: A list of any warnings generated during parsing.
28
+ errors: A list of any errors encountered during parsing.
29
+ source: The origin of the data (e.g., 'local' or 'cloud').
30
+ device_id: The identifier of the device that sent the data.
31
+ frame: A `MonitorFrame` instance if the raw bytes were successfully
32
+ decoded into a low-level frame structure.
33
+ """
34
+ raw: bytes
35
+ raw_b64: str
36
+ received_at: datetime
37
+ parsed: dict[str, Any] = field(default_factory=dict)
38
+ warnings: list[str] = field(default_factory=list)
39
+ errors: list[str] = field(default_factory=list)
40
+ source: str = "local"
41
+ device_id: Optional[str] = None
42
+ frame: Optional[MonitorFrame] = None
@@ -0,0 +1,144 @@
1
+ """
2
+ This module defines the data structures for a "Monitor Profile". A profile is a
3
+ declarative configuration that describes how to interpret the raw bytes of a
4
+ monitor frame for a specific device model.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Dict, Iterable, Optional
10
+
11
+ # A set of valid source fields within the MonitorFrame.
12
+ VALID_SOURCES = {"alarms", "switches", "status", "action", "progress", "accessory"}
13
+
14
+
15
+ @dataclass
16
+ class FlagDefinition:
17
+ """
18
+ Defines how to extract a boolean flag from a specific bit in a byte array.
19
+ This is used for parsing the 'alarms' and 'switches' byte fields.
20
+ """
21
+ source: str
22
+ byte: int
23
+ bit: int
24
+ invert: bool = False
25
+ description: Optional[str] = None
26
+
27
+ def validate(self) -> None:
28
+ """Checks if the definition is valid."""
29
+ if self.source not in {"alarms", "switches"}:
30
+ raise ValueError("flag source must be 'alarms' or 'switches'")
31
+ if self.byte < 0:
32
+ raise ValueError("byte must be non-negative")
33
+ if self.bit < 0 or self.bit > 7:
34
+ raise ValueError("bit must be between 0 and 7")
35
+
36
+
37
+ @dataclass
38
+ class PredicateDefinition:
39
+ """
40
+ Defines a logical condition to be evaluated against the monitor frame data.
41
+ Predicates are used to create higher-level boolean states, like "is_on"
42
+ or "is_making_coffee".
43
+ """
44
+ kind: str
45
+ source: Optional[str] = None
46
+ value: Any = None
47
+ values: Iterable[Any] | None = None
48
+ flag: str | None = None
49
+ byte: int | None = None
50
+ bit: int | None = None
51
+
52
+ def validate(self) -> None:
53
+ """Checks if the predicate definition is valid."""
54
+ if self.kind not in {
55
+ "equals", "not_equals", "in_set", "not_in_set",
56
+ "flag_true", "flag_false", "bit_set", "bit_clear",
57
+ }:
58
+ raise ValueError(f"Unsupported predicate kind: {self.kind}")
59
+ if self.source and self.source not in VALID_SOURCES:
60
+ raise ValueError(f"source must be one of {sorted(VALID_SOURCES)}")
61
+ if self.bit is not None and (self.bit < 0 or self.bit > 7):
62
+ raise ValueError("bit must be between 0 and 7")
63
+
64
+ def uses_flag(self) -> bool:
65
+ """Returns True if the predicate depends on a named flag."""
66
+ return self.kind in {"flag_true", "flag_false"}
67
+
68
+ def uses_bit_address(self) -> bool:
69
+ """Returns True if the predicate directly addresses a bit."""
70
+ return self.kind in {"bit_set", "bit_clear"}
71
+
72
+
73
+ @dataclass
74
+ class MonitorProfile:
75
+ """
76
+ A complete profile for parsing a device's monitor frame.
77
+
78
+ This class holds all the definitions needed to translate the raw bytes of a
79
+ monitor frame into meaningful, human-readable data. It is typically loaded
80
+ from a device-specific JSON file.
81
+ """
82
+ flags: Dict[str, FlagDefinition] = field(default_factory=dict)
83
+ enums: Dict[str, Dict[int, str]] = field(default_factory=dict)
84
+ predicates: Dict[str, PredicateDefinition] = field(default_factory=dict)
85
+
86
+ @classmethod
87
+ def from_dict(cls, data: dict | None) -> "MonitorProfile":
88
+ """
89
+ Constructs a MonitorProfile from a dictionary (e.g., from a JSON file).
90
+ """
91
+ if not data:
92
+ return cls()
93
+
94
+ # --- Load Flag Definitions ---
95
+ flags = {}
96
+ for name, flag_data in (data.get("flags") or {}).items():
97
+ flag = FlagDefinition(
98
+ source=flag_data.get("source"),
99
+ byte=int(flag_data.get("byte", 0)),
100
+ bit=int(flag_data.get("bit", 0)),
101
+ invert=bool(flag_data.get("invert", False)),
102
+ description=flag_data.get("description"),
103
+ )
104
+ flag.validate()
105
+ flags[name] = flag
106
+
107
+ # --- Load Predicate Definitions ---
108
+ predicates = {}
109
+ for name, pred_data in (data.get("predicates") or {}).items():
110
+ pred = PredicateDefinition(
111
+ kind=pred_data.get("kind"),
112
+ source=pred_data.get("source"),
113
+ value=pred_data.get("value"),
114
+ values=pred_data.get("values") or pred_data.get("set") or pred_data.get("in"),
115
+ flag=pred_data.get("flag"),
116
+ byte=pred_data.get("byte"),
117
+ bit=pred_data.get("bit"),
118
+ )
119
+ pred.validate()
120
+ predicates[name] = pred
121
+
122
+ # --- Load Enum Mappings ---
123
+ enums = {
124
+ name: {int(k): v for k, v in (mapping or {}).items()}
125
+ for name, mapping in (data.get("enums", {}) or {}).items()
126
+ }
127
+
128
+ return cls(flags=flags, enums=enums, predicates=predicates)
129
+
130
+ def available_fields(self) -> list[str]:
131
+ """Returns a sorted list of all defined flag and predicate names."""
132
+ dynamic = list(self.flags.keys()) + list(self.predicates.keys())
133
+ return sorted(set(dynamic))
134
+
135
+ def summary(self) -> dict[str, Any]:
136
+ """Provides a brief summary of the profile's contents."""
137
+ return {
138
+ "flags": list(self.flags.keys()),
139
+ "enums": {name: list(mapping.keys()) for name, mapping in self.enums.items()},
140
+ "predicates": list(self.predicates.keys()),
141
+ }
142
+
143
+
144
+ __all__ = ["FlagDefinition", "PredicateDefinition", "MonitorProfile"]
@@ -0,0 +1,196 @@
1
+ """
2
+ This module provides the `MonitorView` class, which offers a high-level,
3
+ user-friendly interface for accessing data from a `MonitorSnapshot`.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Optional
8
+
9
+ from cremalink.core.binary import get_bit
10
+ from cremalink.parsing.monitor.frame import MonitorFrame
11
+ from cremalink.parsing.monitor.model import MonitorSnapshot
12
+ from cremalink.parsing.monitor.profile import MonitorProfile, PredicateDefinition
13
+
14
+
15
+ class MonitorView:
16
+ """
17
+ A user-friendly view of a `MonitorSnapshot`, powered by a `MonitorProfile`.
18
+
19
+ This class acts as a wrapper around a `MonitorSnapshot`. It uses a given
20
+ `MonitorProfile` to translate raw, low-level data (like integer codes and
21
+ bit flags) into human-readable values (like enum names and boolean predicates).
22
+
23
+ It provides dynamic attribute access (`__getattr__`) to resolve flags and
24
+ predicates from the profile on the fly. For example, `view.is_on` or
25
+ `view.has_descaling_alarm`.
26
+ """
27
+
28
+ def __init__(self, snapshot: MonitorSnapshot, profile: MonitorProfile | dict[str, Any] | None = None) -> None:
29
+ """
30
+ Initializes the MonitorView.
31
+
32
+ Args:
33
+ snapshot: The `MonitorSnapshot` containing the data to be viewed.
34
+ profile: A `MonitorProfile` or a dictionary to build one from. This
35
+ profile dictates how the raw data is interpreted.
36
+ """
37
+ self.snapshot = snapshot
38
+ # Ensure profile is always a MonitorProfile instance.
39
+ self.profile = profile if isinstance(profile, MonitorProfile) else MonitorProfile.from_dict(profile or {})
40
+
41
+ # Attempt to get or parse the low-level MonitorFrame.
42
+ self._frame: Optional[MonitorFrame] = snapshot.frame
43
+ if self._frame is None and snapshot.raw_b64:
44
+ try:
45
+ self._frame = MonitorFrame.from_b64(snapshot.raw_b64)
46
+ except Exception:
47
+ self._frame = None
48
+
49
+ # --- raw accessors ---
50
+ @property
51
+ def raw(self) -> bytes:
52
+ """The raw bytes of the monitor payload."""
53
+ return self.snapshot.raw
54
+
55
+ @property
56
+ def raw_b64(self) -> str:
57
+ """The base64-encoded string of the monitor payload."""
58
+ return self.snapshot.raw_b64
59
+
60
+ @property
61
+ def parsed(self) -> dict[str, Any]:
62
+ """The dictionary of initially parsed, low-level fields."""
63
+ return self.snapshot.parsed
64
+
65
+ @property
66
+ def received_at(self):
67
+ """The timestamp when the snapshot was received."""
68
+ return self.snapshot.received_at
69
+
70
+ # --- standard fields ---
71
+ @property
72
+ def status_code(self) -> Optional[int]:
73
+ """The raw integer code for the device's main status."""
74
+ return self._frame.status if self._frame else None
75
+
76
+ @property
77
+ def action_code(self) -> Optional[int]:
78
+ """The raw integer code for the device's current action."""
79
+ return self._frame.action if self._frame else None
80
+
81
+ @property
82
+ def progress_percent(self) -> Optional[int]:
83
+ """The progress percentage (0-100) of the current action."""
84
+ return self._frame.progress if self._frame else None
85
+
86
+ @property
87
+ def accessory_code(self) -> Optional[int]:
88
+ """The raw integer code for the currently detected accessory."""
89
+ return self._frame.accessory if self._frame else None
90
+
91
+ # --- enum mapping ---
92
+ def _enum_lookup(self, enum_name: str, code: Optional[int]) -> Optional[str]:
93
+ """Looks up an enum name from a code using the profile."""
94
+ if code is None:
95
+ return None
96
+ mapping = self.profile.enums.get(enum_name, {})
97
+ # Return the string name, or the original code as a string if not found.
98
+ return mapping.get(int(code), str(code))
99
+
100
+ @property
101
+ def status_name(self) -> Optional[str]:
102
+ """The human-readable name of the device's status (e.g., 'Standby')."""
103
+ return self._enum_lookup("status", self.status_code)
104
+
105
+ @property
106
+ def action_name(self) -> Optional[str]:
107
+ """The human-readable name of the device's action (e.g., 'Brewing')."""
108
+ return self._enum_lookup("action", self.action_code)
109
+
110
+ @property
111
+ def accessory_name(self) -> Optional[str]:
112
+ """The human-readable name of the accessory (e.g., 'Milk Carafe')."""
113
+ return self._enum_lookup("accessory", self.accessory_code)
114
+
115
+ # --- flag/predicate helpers ---
116
+ def _resolve_flag(self, flag_name: str) -> Optional[bool]:
117
+ """Resolves a named boolean flag using its definition in the profile."""
118
+ if not self._frame:
119
+ return None
120
+ flag_def = self.profile.flags.get(flag_name)
121
+ if not flag_def:
122
+ return None
123
+
124
+ data_bytes = self._frame.alarms if flag_def.source == "alarms" else self._frame.switches
125
+ if flag_def.byte >= len(data_bytes):
126
+ return None
127
+
128
+ byte_val = data_bytes[flag_def.byte]
129
+ value = get_bit(byte_val, flag_def.bit)
130
+ return not value if flag_def.invert else value
131
+
132
+ def _source_value(self, source: str) -> Any:
133
+ """Gets a raw value from the frame by its source name."""
134
+ if not self._frame:
135
+ return None
136
+ return {
137
+ "alarms": self._frame.alarms,
138
+ "switches": self._frame.switches,
139
+ "status": self._frame.status,
140
+ "action": self._frame.action,
141
+ "progress": self._frame.progress,
142
+ "accessory": self._frame.accessory,
143
+ }.get(source)
144
+
145
+ def _evaluate_predicate(self, definition: PredicateDefinition) -> Optional[bool]:
146
+ """Evaluates a named predicate using its definition in the profile."""
147
+ try:
148
+ if definition.uses_flag():
149
+ flag_value = self._resolve_flag(definition.flag or "")
150
+ if flag_value is None:
151
+ return None
152
+ return flag_value if definition.kind == "flag_true" else not flag_value
153
+
154
+ if definition.uses_bit_address():
155
+ if not self._frame or not definition.source:
156
+ return None
157
+ source_bytes = self._frame.alarms if definition.source == "alarms" else self._frame.switches
158
+ if definition.byte is None or definition.byte >= len(source_bytes) or definition.bit is None:
159
+ return None
160
+ bit_value = get_bit(source_bytes[definition.byte], definition.bit)
161
+ return bit_value if definition.kind == "bit_set" else not bit_value
162
+
163
+ source_val = self._source_value(definition.source) if definition.source else None
164
+ if definition.kind == "equals":
165
+ return source_val == definition.value
166
+ if definition.kind == "not_equals":
167
+ return source_val != definition.value
168
+ if definition.kind == "in_set":
169
+ return source_val in set(definition.values or [])
170
+ if definition.kind == "not_in_set":
171
+ return source_val not in set(definition.values or [])
172
+ except Exception:
173
+ return None
174
+ return None
175
+
176
+ # --- dynamic access ---
177
+ @property
178
+ def available_fields(self) -> list[str]:
179
+ """A list of all available dynamic fields (flags and predicates)."""
180
+ return self.profile.available_fields()
181
+
182
+ @property
183
+ def profile_summary(self) -> dict[str, Any]:
184
+ """A summary of the loaded profile."""
185
+ return self.profile.summary()
186
+
187
+ def __getattr__(self, item: str) -> Any:
188
+ """
189
+ Dynamically resolves flags and predicates from the profile.
190
+ This allows for accessing profile-defined fields like `view.is_on`.
191
+ """
192
+ if item in self.profile.flags:
193
+ return self._resolve_flag(item)
194
+ if item in self.profile.predicates:
195
+ return self._evaluate_predicate(self.profile.predicates[item])
196
+ raise AttributeError(f"{self.__class__.__name__} has no attribute '{item}'")
@@ -0,0 +1,9 @@
1
+ """
2
+ This package handles the parsing and decoding of device 'properties'.
3
+
4
+ Properties are key-value pairs that represent the static or semi-static
5
+ attributes of the coffee machine, such as configuration settings or counters.
6
+ """
7
+ from cremalink.parsing.properties.decode import PropertiesSnapshot
8
+
9
+ __all__ = ["PropertiesSnapshot"]
@@ -0,0 +1,53 @@
1
+ """
2
+ This module provides classes for handling and decoding device properties.
3
+ """
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from typing import Any, Optional
9
+
10
+
11
+ @dataclass
12
+ class PropertiesSnapshot:
13
+ """
14
+ A container for a snapshot of device properties at a specific time.
15
+
16
+ This class holds the raw property data as received from the device,
17
+ the timestamp of when it was received, and a dictionary for any parsed
18
+ or processed values. It provides a helper method to easily access
19
+ property values from the potentially nested raw data structure.
20
+
21
+ Attributes:
22
+ raw: The raw dictionary of properties from the device.
23
+ received_at: The timestamp when the snapshot was taken.
24
+ parsed: A dictionary to hold processed or parsed property values.
25
+ """
26
+ raw: dict[str, Any]
27
+ received_at: Optional[datetime]
28
+ parsed: dict[str, Any] = field(default_factory=dict)
29
+
30
+ def get(self, name: str) -> Any:
31
+ """
32
+ Safely retrieves a property by its name from the raw data.
33
+
34
+ The properties data can come in different formats. This method checks
35
+ for the property name as a direct key and also searches within the
36
+ nested 'property' objects that are common in the API response.
37
+
38
+ Args:
39
+ name: The name of the property to retrieve.
40
+
41
+ Returns:
42
+ The property dictionary if found, otherwise None.
43
+ """
44
+ # First, check if the name is a top-level key in the raw dictionary.
45
+ if name in self.raw:
46
+ return self.raw[name]
47
+
48
+ # If not, iterate through the values to find a nested property object.
49
+ # This handles the common format: `{'some_id': {'property': {'name': name, ...}}}`
50
+ for entry in self.raw.values():
51
+ if isinstance(entry, dict) and entry.get("property", {}).get("name") == name:
52
+ return entry
53
+ return None
@@ -0,0 +1,10 @@
1
+ """
2
+ This package contains static resources and configuration files for the cremalink
3
+ library, such as API keys and language mappings.
4
+
5
+ It provides helper functions to access these resources in a way that is
6
+ compatible with standard Python packaging.
7
+ """
8
+ from cremalink.resources.api_config import load_api_config
9
+
10
+ __all__ = ["load_api_config"]
@@ -0,0 +1,14 @@
1
+ {
2
+ "GIGYA": {
3
+ "API_KEY": "3_e5qn7USZK-QtsIso1wCelqUKAK_IVEsYshRIssQ-X-k55haiZXmKWDHDRul2e5Y2",
4
+ "CLIENT_ID": "1S8q1WJEs-emOB43Z0-66WnL",
5
+ "CLIENT_SECRET": "lmnceiD0B-4KPNN5ZS6WuWU70j9V5BCuSlz2OPsvHkyLryhMkJkPvKsivfTq3RfNYj8GpCELtOBvhaDIzKcBtg",
6
+ "SDK_BUILD": "16650"
7
+ },
8
+ "AYLA": {
9
+ "APP_ID": "DeLonghiComfort2-mw-id",
10
+ "APP_SECRET": "DeLonghiComfort2-Yg4miiqiNcf0Or-EhJwRh7ACfBY",
11
+ "API_URL": "https://ads-eu.aylanetworks.com/apiv1",
12
+ "OAUTH_URL": "https://user-field-eu.aylanetworks.com"
13
+ }
14
+ }