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.
- cremalink/__init__.py +33 -0
- cremalink/clients/__init__.py +10 -0
- cremalink/clients/cloud.py +130 -0
- cremalink/core/__init__.py +6 -0
- cremalink/core/binary.py +102 -0
- cremalink/crypto/__init__.py +142 -0
- cremalink/devices/AY008ESP1.json +114 -0
- cremalink/devices/__init__.py +116 -0
- cremalink/domain/__init__.py +11 -0
- cremalink/domain/device.py +245 -0
- cremalink/domain/factory.py +98 -0
- cremalink/local_server.py +76 -0
- cremalink/local_server_app/__init__.py +20 -0
- cremalink/local_server_app/api.py +272 -0
- cremalink/local_server_app/config.py +64 -0
- cremalink/local_server_app/device_adapter.py +96 -0
- cremalink/local_server_app/jobs.py +104 -0
- cremalink/local_server_app/logging.py +116 -0
- cremalink/local_server_app/models.py +76 -0
- cremalink/local_server_app/protocol.py +135 -0
- cremalink/local_server_app/state.py +358 -0
- cremalink/parsing/__init__.py +7 -0
- cremalink/parsing/monitor/__init__.py +22 -0
- cremalink/parsing/monitor/decode.py +79 -0
- cremalink/parsing/monitor/extractors.py +69 -0
- cremalink/parsing/monitor/frame.py +132 -0
- cremalink/parsing/monitor/model.py +42 -0
- cremalink/parsing/monitor/profile.py +144 -0
- cremalink/parsing/monitor/view.py +196 -0
- cremalink/parsing/properties/__init__.py +9 -0
- cremalink/parsing/properties/decode.py +53 -0
- cremalink/resources/__init__.py +10 -0
- cremalink/resources/api_config.json +14 -0
- cremalink/resources/api_config.py +30 -0
- cremalink/resources/lang.json +223 -0
- cremalink/transports/__init__.py +7 -0
- cremalink/transports/base.py +94 -0
- cremalink/transports/cloud/__init__.py +9 -0
- cremalink/transports/cloud/transport.py +166 -0
- cremalink/transports/local/__init__.py +9 -0
- cremalink/transports/local/transport.py +164 -0
- cremalink-0.1.0b5.dist-info/METADATA +138 -0
- cremalink-0.1.0b5.dist-info/RECORD +47 -0
- cremalink-0.1.0b5.dist-info/WHEEL +5 -0
- cremalink-0.1.0b5.dist-info/entry_points.txt +2 -0
- cremalink-0.1.0b5.dist-info/licenses/LICENSE +661 -0
- cremalink-0.1.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides a function to load the API configuration from the
|
|
3
|
+
embedded `api_config.json` resource file.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
from importlib import resources
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@lru_cache
|
|
14
|
+
def load_api_config() -> dict[str, Any]:
|
|
15
|
+
"""
|
|
16
|
+
Loads the API configuration from the `api_config.json` resource file.
|
|
17
|
+
|
|
18
|
+
This function uses `importlib.resources` to access the JSON file.
|
|
19
|
+
|
|
20
|
+
The `@lru_cache` decorator memoizes the result, so the file is only read
|
|
21
|
+
and parsed once, improving performance on subsequent calls.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A dictionary containing the API configuration data.
|
|
25
|
+
"""
|
|
26
|
+
# Locate the 'api_config.json' file within the 'cremalink.resources' package.
|
|
27
|
+
resource = resources.files("cremalink.resources").joinpath("api_config.json")
|
|
28
|
+
# Open the resource file and load its JSON content.
|
|
29
|
+
with resource.open("r", encoding="utf-8") as f:
|
|
30
|
+
return json.load(f)
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
{
|
|
2
|
+
"language_comms_keys": {
|
|
3
|
+
"AE": "profiledCommunicationAE",
|
|
4
|
+
"AR": "profiledCommunicationCL",
|
|
5
|
+
"AT": "profiledCommunicationAT",
|
|
6
|
+
"AU": "profiledCommunicationAU",
|
|
7
|
+
"BD": "profiledCommunicationHKBDKHTH",
|
|
8
|
+
"BE": "profiledCommunicationBE",
|
|
9
|
+
"BG": "profiledCommunicationHRRSSIBG",
|
|
10
|
+
"BH": "profiledCommunicationAE",
|
|
11
|
+
"BR": "profiledCommunicationBR",
|
|
12
|
+
"CA": "profiledCommunicationCA",
|
|
13
|
+
"CH": "profiledCommunicationCH",
|
|
14
|
+
"CL": "profiledCommunicationCL",
|
|
15
|
+
"CO": "profiledCommunicationMXCO",
|
|
16
|
+
"CZ": "profiledCommunicationCZSKHU",
|
|
17
|
+
"DE": "profiledCommunicationDE",
|
|
18
|
+
"DK": "profiledCommunicationSEFINODK",
|
|
19
|
+
"EE": "profiledCommunicationPLEELTLV",
|
|
20
|
+
"EG": "profiledCommunicationAE",
|
|
21
|
+
"ES": "profiledCommunicationES",
|
|
22
|
+
"FI": "profiledCommunicationSEFINODK",
|
|
23
|
+
"FR": "profiledCommunicationFR",
|
|
24
|
+
"GB": "profiledCommunicationGB",
|
|
25
|
+
"GR": "profiledCommunicationGR",
|
|
26
|
+
"HK": "profiledCommunicationHKBDKHTH",
|
|
27
|
+
"HR": "profiledCommunicationHRRSSIBG",
|
|
28
|
+
"HU": "profiledCommunicationCZSKHU",
|
|
29
|
+
"ID": "profiledCommunicationHKBDKHTH",
|
|
30
|
+
"IE": "profiledCommunicationGB",
|
|
31
|
+
"IL": "profiledCommunicationAE",
|
|
32
|
+
"IN": "profiledCommunicationAE",
|
|
33
|
+
"IR": "profiledCommunicationAE",
|
|
34
|
+
"IT": "profiledCommunicationIT",
|
|
35
|
+
"JP": "profiledCommunicationJP",
|
|
36
|
+
"KH": "profiledCommunicationHKBDKHTH",
|
|
37
|
+
"KR": "profiledCommunicationKR",
|
|
38
|
+
"KW": "profiledCommunicationAE",
|
|
39
|
+
"LT": "profiledCommunicationPLEELTLV",
|
|
40
|
+
"LU": "profiledCommunicationBE",
|
|
41
|
+
"LV": "profiledCommunicationPLEELTLV",
|
|
42
|
+
"MT": "profiledCommunicationHRRSSIBG",
|
|
43
|
+
"MX": "profiledCommunicationMXCO",
|
|
44
|
+
"MY": "profiledCommunicationMY",
|
|
45
|
+
"NL": "profiledCommunicationNL",
|
|
46
|
+
"NO": "profiledCommunicationSEFINODK",
|
|
47
|
+
"NZ": "profiledCommunicationNZ",
|
|
48
|
+
"OM": "profiledCommunicationAE",
|
|
49
|
+
"PE": "profiledCommunicationCL",
|
|
50
|
+
"PH": "profiledCommunicationHKBDKHTH",
|
|
51
|
+
"PL": "profiledCommunicationPLEELTLV",
|
|
52
|
+
"PT": "profiledCommunicationPT",
|
|
53
|
+
"QA": "profiledCommunicationAE",
|
|
54
|
+
"RO": "profiledCommunicationRO",
|
|
55
|
+
"RS": "profiledCommunicationHRRSSIBG",
|
|
56
|
+
"SA": "profiledCommunicationAE",
|
|
57
|
+
"SE": "profiledCommunicationSEFINODK",
|
|
58
|
+
"SG": "profiledCommunicationSG",
|
|
59
|
+
"SI": "profiledCommunicationHRRSSIBG",
|
|
60
|
+
"SK": "profiledCommunicationCZSKHU",
|
|
61
|
+
"TH": "profiledCommunicationHKBDKHTH",
|
|
62
|
+
"TR": "profiledCommunicationTR",
|
|
63
|
+
"TW": "profiledCommunicationHKBDKHTH",
|
|
64
|
+
"UA": "profiledCommunicationUA",
|
|
65
|
+
"US": "profiledCommunicationUS",
|
|
66
|
+
"VN": "profiledCommunicationHKBDKHTH",
|
|
67
|
+
"ZA": "profiledCommunicationZA"
|
|
68
|
+
},
|
|
69
|
+
"language_countries": {
|
|
70
|
+
"ar-ae": "United Arab Emirates",
|
|
71
|
+
"ar-eg": "Egypt",
|
|
72
|
+
"ar-sa": "Saudi Arabia",
|
|
73
|
+
"br": "Bulgaria",
|
|
74
|
+
"cs": "Czechia",
|
|
75
|
+
"da": "Denmark",
|
|
76
|
+
"de": "Germany",
|
|
77
|
+
"de-at": "Austria",
|
|
78
|
+
"de-inf": "Switzerland",
|
|
79
|
+
"el": "Greece",
|
|
80
|
+
"en": "United Kingdom",
|
|
81
|
+
"en-ae": "United Arab Emirates",
|
|
82
|
+
"en-au": "Australia",
|
|
83
|
+
"en-bd": "Bangladesh",
|
|
84
|
+
"en-bh": "Bahrain",
|
|
85
|
+
"en-ca": "Canada",
|
|
86
|
+
"en-eg": "Egypt",
|
|
87
|
+
"en-hk": "Hong Kong",
|
|
88
|
+
"en-id": "Indonesia",
|
|
89
|
+
"en-ie": "Ireland",
|
|
90
|
+
"en-il": "Israel",
|
|
91
|
+
"en-in": "India",
|
|
92
|
+
"en-ir": "Iran",
|
|
93
|
+
"en-kh": "Cambodia",
|
|
94
|
+
"en-kw": "Kuwait",
|
|
95
|
+
"en-mt": "Malta",
|
|
96
|
+
"en-my": "Malaysia",
|
|
97
|
+
"en-nz": "New Zealand",
|
|
98
|
+
"en-om": "Oman",
|
|
99
|
+
"en-ph": "Philippines",
|
|
100
|
+
"en-qa": "Qatar",
|
|
101
|
+
"en-sa": "Saudi Arabia",
|
|
102
|
+
"en-sg": "Singapore",
|
|
103
|
+
"en-th": "Thailand",
|
|
104
|
+
"en-us": "United States",
|
|
105
|
+
"en-za": "South Africa",
|
|
106
|
+
"es": "Spain",
|
|
107
|
+
"es-ar": "Argentina",
|
|
108
|
+
"es-cl": "Chile",
|
|
109
|
+
"es-co": "Colombia",
|
|
110
|
+
"es-mx": "Mexico",
|
|
111
|
+
"es-pe": "Peru",
|
|
112
|
+
"et-ee": "Estonia",
|
|
113
|
+
"fa": "Iran",
|
|
114
|
+
"fi": "Finland",
|
|
115
|
+
"fr": "France",
|
|
116
|
+
"fr-be": "Belgium",
|
|
117
|
+
"fr-ca": "Canada",
|
|
118
|
+
"fr-ch": "Switzerland",
|
|
119
|
+
"hr": "Croatia",
|
|
120
|
+
"hu": "Hungary",
|
|
121
|
+
"it": "Italy",
|
|
122
|
+
"ja": "Japan",
|
|
123
|
+
"ko": "South Korea",
|
|
124
|
+
"lt-lt": "Lithuania",
|
|
125
|
+
"lu": "Luxembourg",
|
|
126
|
+
"lv-lv": "Latvia",
|
|
127
|
+
"mt-mt": "Malta",
|
|
128
|
+
"my": "Malaysia",
|
|
129
|
+
"nl": "Netherlands",
|
|
130
|
+
"nl-inf": "Belgium",
|
|
131
|
+
"no": "Norway",
|
|
132
|
+
"pl": "Poland",
|
|
133
|
+
"pt": "Portugal",
|
|
134
|
+
"pt-br": "Brazil",
|
|
135
|
+
"ro": "Romania",
|
|
136
|
+
"sk": "Slovakia",
|
|
137
|
+
"sl": "Slovenia",
|
|
138
|
+
"sr": "Serbia",
|
|
139
|
+
"sv": "Sweden",
|
|
140
|
+
"th": "Thailand",
|
|
141
|
+
"tr": "Turkey",
|
|
142
|
+
"uk": "Ukraine",
|
|
143
|
+
"vi": "Vietnam",
|
|
144
|
+
"zh-tw": "Taiwan"
|
|
145
|
+
},
|
|
146
|
+
"languages": {
|
|
147
|
+
"ar-ae": "AE",
|
|
148
|
+
"ar-eg": "EG",
|
|
149
|
+
"ar-sa": "SA",
|
|
150
|
+
"br": "BG",
|
|
151
|
+
"cs": "CZ",
|
|
152
|
+
"da": "DK",
|
|
153
|
+
"de": "DE",
|
|
154
|
+
"de-at": "AT",
|
|
155
|
+
"de-inf": "CH",
|
|
156
|
+
"el": "GR",
|
|
157
|
+
"en": "GB",
|
|
158
|
+
"en-ae": "AE",
|
|
159
|
+
"en-au": "AU",
|
|
160
|
+
"en-bd": "BD",
|
|
161
|
+
"en-bh": "BH",
|
|
162
|
+
"en-ca": "CA",
|
|
163
|
+
"en-eg": "EG",
|
|
164
|
+
"en-hk": "HK",
|
|
165
|
+
"en-id": "ID",
|
|
166
|
+
"en-ie": "IE",
|
|
167
|
+
"en-il": "IL",
|
|
168
|
+
"en-in": "IN",
|
|
169
|
+
"en-ir": "IR",
|
|
170
|
+
"en-kh": "KH",
|
|
171
|
+
"en-kw": "KW",
|
|
172
|
+
"en-mt": "MT",
|
|
173
|
+
"en-my": "MY",
|
|
174
|
+
"en-nz": "NZ",
|
|
175
|
+
"en-om": "OM",
|
|
176
|
+
"en-ph": "PH",
|
|
177
|
+
"en-qa": "QA",
|
|
178
|
+
"en-sa": "SA",
|
|
179
|
+
"en-sg": "SG",
|
|
180
|
+
"en-th": "TH",
|
|
181
|
+
"en-us": "US",
|
|
182
|
+
"en-za": "ZA",
|
|
183
|
+
"es": "ES",
|
|
184
|
+
"es-ar": "AR",
|
|
185
|
+
"es-cl": "CL",
|
|
186
|
+
"es-co": "CO",
|
|
187
|
+
"es-mx": "MX",
|
|
188
|
+
"es-pe": "PE",
|
|
189
|
+
"et-ee": "EE",
|
|
190
|
+
"fa": "IR",
|
|
191
|
+
"fi": "FI",
|
|
192
|
+
"fr": "FR",
|
|
193
|
+
"fr-be": "BE",
|
|
194
|
+
"fr-ca": "CA",
|
|
195
|
+
"fr-ch": "CH",
|
|
196
|
+
"hr": "HR",
|
|
197
|
+
"hu": "HU",
|
|
198
|
+
"it": "IT",
|
|
199
|
+
"ja": "JP",
|
|
200
|
+
"ko": "KR",
|
|
201
|
+
"lt-lt": "LT",
|
|
202
|
+
"lu": "LU",
|
|
203
|
+
"lv-lv": "LV",
|
|
204
|
+
"mt-mt": "MT",
|
|
205
|
+
"my": "MY",
|
|
206
|
+
"nl": "NL",
|
|
207
|
+
"nl-inf": "BE",
|
|
208
|
+
"no": "NO",
|
|
209
|
+
"pl": "PL",
|
|
210
|
+
"pt": "PT",
|
|
211
|
+
"pt-br": "BR",
|
|
212
|
+
"ro": "RO",
|
|
213
|
+
"sk": "SK",
|
|
214
|
+
"sl": "SI",
|
|
215
|
+
"sr": "RS",
|
|
216
|
+
"sv": "SE",
|
|
217
|
+
"th": "TH",
|
|
218
|
+
"tr": "TR",
|
|
219
|
+
"uk": "UA",
|
|
220
|
+
"vi": "VN",
|
|
221
|
+
"zh-tw": "TW"
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This package contains the different transport implementations for communicating
|
|
3
|
+
with coffee machines, such as via the local network or the cloud.
|
|
4
|
+
|
|
5
|
+
Each sub-package (e.g., `local`, `cloud`) provides a concrete implementation
|
|
6
|
+
of the `DeviceTransport` protocol.
|
|
7
|
+
"""
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines the abstract base protocol for device communication transports.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from typing import Any, Protocol
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DeviceTransport(Protocol):
|
|
10
|
+
"""
|
|
11
|
+
A protocol defining the standard interface for all device transports.
|
|
12
|
+
|
|
13
|
+
A transport is responsible for the low-level details of communicating with a
|
|
14
|
+
coffee machine, whether it's over a local network (HTTP) or via the cloud.
|
|
15
|
+
This class uses `typing.Protocol` to allow for structural subtyping, meaning
|
|
16
|
+
any class that implements these methods will be considered a valid transport.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def configure(self) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Performs any necessary setup or configuration for the transport.
|
|
22
|
+
This could include authentication, connection setup, etc.
|
|
23
|
+
"""
|
|
24
|
+
...
|
|
25
|
+
|
|
26
|
+
def send_command(self, command: str) -> Any:
|
|
27
|
+
"""
|
|
28
|
+
Sends a command to the device.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
command: The command payload to be sent.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
The response from the device, with the format depending on the
|
|
35
|
+
transport implementation.
|
|
36
|
+
"""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def set_mappings(self, command_map: dict[str, Any], property_map: dict[str, Any]) -> None:
|
|
40
|
+
"""
|
|
41
|
+
(Optional) Provides the transport with device-specific command and property maps.
|
|
42
|
+
|
|
43
|
+
This can be used by transports that need to perform lookups or translations.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
command_map: A dictionary mapping command aliases to their details.
|
|
47
|
+
property_map: A dictionary mapping property aliases to their details.
|
|
48
|
+
"""
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
def get_monitor(self) -> Any:
|
|
52
|
+
"""
|
|
53
|
+
Retrieves the current monitoring status data from the device.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The raw monitoring data.
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def refresh_monitor(self) -> Any:
|
|
61
|
+
"""
|
|
62
|
+
Requests the device to send an updated monitoring status.
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def get_properties(self) -> Any:
|
|
67
|
+
"""
|
|
68
|
+
Fetches a set of properties from the device.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
A collection of device properties.
|
|
72
|
+
"""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
def get_property(self, name: str) -> Any:
|
|
76
|
+
"""
|
|
77
|
+
Fetches a single, specific property from the device.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name: The name of the property to retrieve.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
The value of the requested property.
|
|
84
|
+
"""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
def health(self) -> Any:
|
|
88
|
+
"""
|
|
89
|
+
Performs a health check to verify connectivity with the device.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
A status indicating the health of the connection.
|
|
93
|
+
"""
|
|
94
|
+
...
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This package contains the transport implementation for communicating with
|
|
3
|
+
devices via the cloud.
|
|
4
|
+
|
|
5
|
+
It exposes the `CloudTransport` class as the main entry point.
|
|
6
|
+
"""
|
|
7
|
+
from cremalink.transports.cloud.transport import CloudTransport
|
|
8
|
+
|
|
9
|
+
__all__ = ["CloudTransport"]
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides the `CloudTransport` class, which handles communication
|
|
3
|
+
with a coffee machine via the manufacturer's cloud API (Ayla Networks).
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from cremalink.parsing.monitor.decode import build_monitor_snapshot
|
|
13
|
+
from cremalink.transports.base import DeviceTransport
|
|
14
|
+
from cremalink.resources import load_api_config
|
|
15
|
+
|
|
16
|
+
API_USER_AGENT = "datatransport/3.1.2 android/"
|
|
17
|
+
TOKEN_USER_AGENT = "DeLonghiComfort/3 CFNetwork/1568.300.101 Darwin/24.2.0"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CloudTransport(DeviceTransport):
|
|
21
|
+
"""
|
|
22
|
+
A transport for communicating with a device via the cloud API.
|
|
23
|
+
|
|
24
|
+
This transport interacts directly with the Ayla cloud service endpoints,
|
|
25
|
+
using a short-lived access token for authentication. Upon initialization,
|
|
26
|
+
it fetches key device metadata from the cloud and stores it.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, dsn: str, access_token: str, device_map_path: Optional[str] = None) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Initializes the CloudTransport.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
dsn: The Device Serial Number.
|
|
35
|
+
access_token: A valid OAuth access token for the cloud API.
|
|
36
|
+
device_map_path: Optional path to a device-specific command map file.
|
|
37
|
+
"""
|
|
38
|
+
self.api_conf = load_api_config()
|
|
39
|
+
self.gigya_api = self.api_conf.get("GIGYA")
|
|
40
|
+
self.ayla_api = self.api_conf.get("AYLA")
|
|
41
|
+
|
|
42
|
+
self.dsn = dsn
|
|
43
|
+
self.access_token = access_token
|
|
44
|
+
self.device_map_path = device_map_path
|
|
45
|
+
self.command_map: dict[str, Any] = {}
|
|
46
|
+
self.property_map: dict[str, Any] = {}
|
|
47
|
+
|
|
48
|
+
# Fetch device metadata from the cloud immediately upon initialization.
|
|
49
|
+
device = self._get(".json").get("device", {})
|
|
50
|
+
self.id = device.get("key") # The Ayla internal device ID
|
|
51
|
+
self.model = device.get("model")
|
|
52
|
+
self.is_lan_enabled = device.get("lan_enabled", False)
|
|
53
|
+
self.type = device.get("type")
|
|
54
|
+
self.is_online = device.get("connection_status", False) == "Online"
|
|
55
|
+
self.ip = device.get("lan_ip")
|
|
56
|
+
|
|
57
|
+
# Fetch LAN key, which might be needed for other operations.
|
|
58
|
+
lan = self._get("/lan.json") or {}
|
|
59
|
+
self.lan_key = lan.get("lanip", {}).get("lanip_key")
|
|
60
|
+
|
|
61
|
+
def configure(self) -> None:
|
|
62
|
+
"""Configuration is handled during __init__, so this is a no-op."""
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
# ---- helpers ----
|
|
66
|
+
def _get(self, path: str) -> dict:
|
|
67
|
+
"""Helper for making authenticated GET requests using the device DSN."""
|
|
68
|
+
response = requests.get(
|
|
69
|
+
url=f"{self.ayla_api.get('API_URL')}/dsns/{self.dsn}{path}",
|
|
70
|
+
headers={
|
|
71
|
+
"User-Agent": API_USER_AGENT,
|
|
72
|
+
"Authorization": f"auth_token {self.access_token}",
|
|
73
|
+
"Accept": "application/json",
|
|
74
|
+
},
|
|
75
|
+
)
|
|
76
|
+
response.raise_for_status()
|
|
77
|
+
return response.json()
|
|
78
|
+
|
|
79
|
+
def _get_by_id(self, path: str) -> dict:
|
|
80
|
+
"""Helper for making authenticated GET requests using the internal device ID."""
|
|
81
|
+
response = requests.get(
|
|
82
|
+
url=f"{self.ayla_api.get('API_URL')}/devices/{self.id}{path}",
|
|
83
|
+
headers={
|
|
84
|
+
"User-Agent": API_USER_AGENT,
|
|
85
|
+
"Authorization": f"auth_token {self.access_token}",
|
|
86
|
+
"Accept": "application/json",
|
|
87
|
+
},
|
|
88
|
+
)
|
|
89
|
+
response.raise_for_status()
|
|
90
|
+
return response.json()
|
|
91
|
+
|
|
92
|
+
def _post(self, path: str, data: dict) -> dict:
|
|
93
|
+
"""Helper for making authenticated POST requests."""
|
|
94
|
+
response = requests.post(
|
|
95
|
+
url=f"{self.ayla_api.get('API_URL')}/dsns/{self.dsn}{path}",
|
|
96
|
+
headers={
|
|
97
|
+
"User-Agent": API_USER_AGENT,
|
|
98
|
+
"Authorization": f"auth_token {self.access_token}",
|
|
99
|
+
"Accept": "application/json",
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
},
|
|
102
|
+
json=data,
|
|
103
|
+
)
|
|
104
|
+
response.raise_for_status()
|
|
105
|
+
return response.json()
|
|
106
|
+
|
|
107
|
+
# ---- DeviceTransport Implementation ----
|
|
108
|
+
def send_command(self, command: str) -> Any:
|
|
109
|
+
"""Sends a command to the device by creating a new 'datapoint' via the cloud API."""
|
|
110
|
+
payload = {"datapoint": {"value": command}}
|
|
111
|
+
return self._post(path="/properties/data_request/datapoints.json", data=payload)
|
|
112
|
+
|
|
113
|
+
def set_mappings(self, command_map: dict[str, Any], property_map: dict[str, Any]) -> None:
|
|
114
|
+
"""Stores the provided command and property maps on the instance."""
|
|
115
|
+
self.command_map = command_map
|
|
116
|
+
self.property_map = property_map
|
|
117
|
+
|
|
118
|
+
def get_properties(self) -> Any:
|
|
119
|
+
"""Fetches all properties for the device from the cloud API."""
|
|
120
|
+
return self._get("/properties.json")
|
|
121
|
+
|
|
122
|
+
def get_property(self, name: str) -> Any:
|
|
123
|
+
"""Fetches a single, specific property by name."""
|
|
124
|
+
props = self._get(f"/properties.json?names[]={name}")
|
|
125
|
+
# The API returns a list, even for a single property.
|
|
126
|
+
if props and isinstance(props, list):
|
|
127
|
+
return props[0].get("property")
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
def get_monitor(self) -> Any:
|
|
131
|
+
"""
|
|
132
|
+
Fetches, parses, and returns the device's monitoring status.
|
|
133
|
+
|
|
134
|
+
This works by fetching the specific 'monitor' property, extracting its
|
|
135
|
+
base64 value, and then decoding it into a structured snapshot.
|
|
136
|
+
"""
|
|
137
|
+
property_name = self.property_map.get("monitor", "d302_monitor")
|
|
138
|
+
prop = self.get_property(property_name) or {}
|
|
139
|
+
raw_b64 = prop.get("value")
|
|
140
|
+
received_at = prop.get("updated_at")
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
# Convert timestamp string to float if possible, otherwise use current time.
|
|
144
|
+
received_ts = float(received_at) if received_at is not None else time.time()
|
|
145
|
+
except (TypeError, ValueError):
|
|
146
|
+
received_ts = time.time()
|
|
147
|
+
payload = {
|
|
148
|
+
"monitor": {"data": {"value": raw_b64}},
|
|
149
|
+
"monitor_b64": raw_b64,
|
|
150
|
+
"received_at": received_ts,
|
|
151
|
+
}
|
|
152
|
+
return build_monitor_snapshot(payload, source="cloud", device_id=self.dsn or self.id)
|
|
153
|
+
|
|
154
|
+
def refresh_monitor(self) -> Any:
|
|
155
|
+
"""
|
|
156
|
+
The cloud API does not provide a direct way to force a monitor refresh.
|
|
157
|
+
This method is a no-op.
|
|
158
|
+
"""
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def health(self) -> Any:
|
|
162
|
+
"""
|
|
163
|
+
Returns the device's online status as determined during initialization.
|
|
164
|
+
This does not perform a live health check.
|
|
165
|
+
"""
|
|
166
|
+
return {"online": self.is_online}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This package contains the transport implementation for communicating with
|
|
3
|
+
devices over the local network (LAN).
|
|
4
|
+
|
|
5
|
+
It exposes the `LocalTransport` class as the main entry point.
|
|
6
|
+
"""
|
|
7
|
+
from cremalink.transports.local.transport import LocalTransport
|
|
8
|
+
|
|
9
|
+
__all__ = ["LocalTransport"]
|