@tempivo/sensor-beacon 1.1.7 → 1.1.9

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 (40) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +23 -5
  3. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +19 -0
  4. package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +16 -0
  5. package/android-aar/library/build.gradle +1 -0
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +73 -4
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +4 -8
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +19 -0
  9. package/dist/calibration.d.ts +8 -0
  10. package/dist/calibration.js +49 -14
  11. package/dist/cellular-network-status.d.ts +69 -0
  12. package/dist/cellular-network-status.js +292 -0
  13. package/dist/channel-capability.d.ts +43 -0
  14. package/dist/channel-capability.js +108 -0
  15. package/dist/config-errors.d.ts +31 -0
  16. package/dist/config-errors.js +244 -0
  17. package/dist/index.d.ts +14 -2
  18. package/dist/index.js +8 -2
  19. package/dist/native-bridge.d.ts +12 -0
  20. package/dist/native-bridge.js +18 -0
  21. package/dist/native-module.d.ts +19 -0
  22. package/dist/native-module.js +117 -2
  23. package/dist/plmn-by-country.d.ts +14 -0
  24. package/dist/plmn-by-country.js +35 -0
  25. package/dist/plmn-labels.d.ts +14 -0
  26. package/dist/plmn-labels.js +37 -0
  27. package/dist/production-defaults.js +63 -9
  28. package/dist/proto/partner-to-proto.d.ts +0 -4
  29. package/dist/proto/partner-to-proto.js +12 -0
  30. package/dist/react-native.d.ts +23 -2
  31. package/dist/react-native.js +20 -2
  32. package/dist/session-types.d.ts +2 -0
  33. package/dist/tempivo-sensor-beacon.aar +0 -0
  34. package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +4 -4
  35. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +2 -2
  36. package/ios/Sources/TempivoSensorBeacon/TempivoSensorCalibration.swift +13 -10
  37. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +4 -1
  38. package/ios/TempivoSensorBeacon.podspec +2 -1
  39. package/ios/TempivoSensorBeaconModule.swift +2 -0
  40. package/package.json +1 -6
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Parse GATT cellular network status JSON and format short partner-facing labels
3
+ * (same vocabulary as the Tempivo app: No SIM, No internet, No server, …).
4
+ */
5
+ import { getOperatorNameFromPlmn } from './plmn-labels.js';
6
+ const REGISTERED_STATUSES = new Set(['REGISTERED', 'REGISTERED_ROAMING']);
7
+ const OK_COMM_STATUSES = new Set(['OK', 'OK_NO_DATA_TO_SEND']);
8
+ const OK_SIM_CARD_STATUSES = new Set(['OK']);
9
+ const LABELS_EN = {
10
+ statusOk: 'Cellular and server OK.',
11
+ statusServer: (serverStatus) => `Server: ${serverStatus}.`,
12
+ statusCloudPending: 'Cloud contact may take a minute.',
13
+ statusUnregistered: 'Not registered on the network.',
14
+ statusNoIp: 'No IP assigned. Check SIM and coverage.',
15
+ serverOk: 'Server OK',
16
+ noNetwork: 'No network',
17
+ noInternet: 'No internet',
18
+ simError: 'SIM error',
19
+ simMissing: 'No SIM',
20
+ simPin: 'SIM PIN required',
21
+ unavailable: 'Unavailable',
22
+ comm: {
23
+ OK: 'OK',
24
+ OK_NO_DATA_TO_SEND: 'OK',
25
+ UNKNOWN: 'No cloud contact yet',
26
+ DNS_ERROR: 'No server',
27
+ UNAUTHORIZED: 'Server refused',
28
+ TIMEOUT: 'Timeout',
29
+ NOT_REGISTERED: 'Not registered',
30
+ NO_COVERAGE: 'No coverage',
31
+ SERVER_ERROR: 'Server error',
32
+ UNEXPECTED_ERROR: 'Send error',
33
+ },
34
+ };
35
+ const LABELS_SV = {
36
+ statusOk: 'Mobil och server OK.',
37
+ statusServer: (serverStatus) => `Server: ${serverStatus}.`,
38
+ statusCloudPending: 'Molnkontakt kan ta en minut.',
39
+ statusUnregistered: 'Ej registrerat på nätet.',
40
+ statusNoIp: 'Ingen IP. Kontrollera SIM och täckning.',
41
+ serverOk: 'Server OK',
42
+ noNetwork: 'Inget nät',
43
+ noInternet: 'Inget internet',
44
+ simError: 'SIM-fel',
45
+ simMissing: 'Inget SIM',
46
+ simPin: 'SIM PIN krävs',
47
+ unavailable: 'Otillgänglig',
48
+ comm: {
49
+ OK: 'OK',
50
+ OK_NO_DATA_TO_SEND: 'OK',
51
+ UNKNOWN: 'Ingen molnkontakt än',
52
+ DNS_ERROR: 'Ingen server',
53
+ UNAUTHORIZED: 'Server nekade',
54
+ TIMEOUT: 'Timeout',
55
+ NOT_REGISTERED: 'Ej registrerat',
56
+ NO_COVERAGE: 'Ingen täckning',
57
+ SERVER_ERROR: 'Serverfel',
58
+ UNEXPECTED_ERROR: 'Sändfel',
59
+ },
60
+ };
61
+ function labelsFor(locale) {
62
+ return locale === 'sv' ? LABELS_SV : LABELS_EN;
63
+ }
64
+ export function isValidCellularIpAddress(ip) {
65
+ if (!ip?.trim())
66
+ return false;
67
+ const normalized = ip.trim();
68
+ if (normalized === '0.0.0.0' || normalized === '0')
69
+ return false;
70
+ return !/^0(\.0){3}$/.test(normalized);
71
+ }
72
+ function readIpAddress(raw) {
73
+ let ip = null;
74
+ if (typeof raw === 'string' && raw.trim())
75
+ ip = raw.trim();
76
+ else if (raw && typeof raw === 'object' && 'value' in raw) {
77
+ const v = raw.value;
78
+ if (typeof v === 'string' && v.trim())
79
+ ip = v.trim();
80
+ }
81
+ return isValidCellularIpAddress(ip) ? ip : null;
82
+ }
83
+ function readStringField(obj, key) {
84
+ const v = obj[key];
85
+ return typeof v === 'string' && v.trim() ? v.trim() : null;
86
+ }
87
+ function readNumberField(obj, key) {
88
+ const v = obj[key];
89
+ return typeof v === 'number' && Number.isFinite(v) ? v : null;
90
+ }
91
+ /** Legacy 0–63 level → dBm (same mapping as Tempivo ingestion). */
92
+ function rssiLevelToDbm(level) {
93
+ const L = Math.max(0, Math.min(63, Math.round(level)));
94
+ if (L <= 15)
95
+ return -110 + L;
96
+ if (L <= 25)
97
+ return -94 + (L - 16);
98
+ if (L <= 35)
99
+ return -84 + (L - 26);
100
+ return -74 + ((L - 36) * 26) / 27;
101
+ }
102
+ export function normalizeCellularSignalStrengthDbm(raw) {
103
+ if (raw == null || !Number.isFinite(raw) || raw === 0 || raw === 99)
104
+ return null;
105
+ if (raw >= 0 && raw <= 63)
106
+ return Math.round(rssiLevelToDbm(raw));
107
+ return Math.round(raw);
108
+ }
109
+ function readEclField(obj) {
110
+ for (const key of ['ecl', 'extendedCoverageLevel', 'extendedCoverage', 'coverageExtensionLevel']) {
111
+ const v = obj[key];
112
+ const n = typeof v === 'number' && Number.isFinite(v) ? v : Number(v);
113
+ if (n === 0 || n === 1 || n === 2)
114
+ return n;
115
+ }
116
+ return null;
117
+ }
118
+ function readPlmnField(obj, key) {
119
+ const v = obj[key];
120
+ if (typeof v === 'number' && Number.isFinite(v) && v >= 10_000 && v <= 999_999)
121
+ return Math.trunc(v);
122
+ if (typeof v === 'string' && /^\d{5,6}$/.test(v.trim()))
123
+ return Number.parseInt(v.trim(), 10);
124
+ return null;
125
+ }
126
+ export function parseCellularNetworkStatusObject(obj, legacySession = false) {
127
+ const signalRaw = readNumberField(obj, 'signalStrength') ?? readNumberField(obj, 'rssi');
128
+ const rsrpRaw = readNumberField(obj, 'rsrp') ?? readNumberField(obj, 'referenceSignalReceivedPower');
129
+ return {
130
+ ipAddress: readIpAddress(obj.ipAddress),
131
+ registrationStatus: readStringField(obj, 'registrationStatus'),
132
+ serverCommunicationStatus: readStringField(obj, 'serverCommunicationStatus'),
133
+ communicationStatus: readStringField(obj, 'communicationStatus'),
134
+ signalStrengthDbm: normalizeCellularSignalStrengthDbm(signalRaw),
135
+ ecl: readEclField(obj),
136
+ rsrpDbm: rsrpRaw == null || !Number.isFinite(rsrpRaw) || rsrpRaw === 0 ? null : Math.round(rsrpRaw),
137
+ simCardStatus: readStringField(obj, 'simCardStatus'),
138
+ registeredPlmn: readPlmnField(obj, 'rplmn'),
139
+ legacySession,
140
+ };
141
+ }
142
+ /** Parse Gson/SDK JSON from {@link getCellularNetworkStatusJson}. */
143
+ export function parseCellularNetworkStatusJson(json, legacySession = false) {
144
+ const trimmed = json.trim();
145
+ if (!trimmed)
146
+ return null;
147
+ try {
148
+ const obj = JSON.parse(trimmed);
149
+ return parseCellularNetworkStatusObject(obj, legacySession);
150
+ }
151
+ catch {
152
+ return null;
153
+ }
154
+ }
155
+ export function isCellularRegistrationOk(status) {
156
+ return status.registrationStatus != null && REGISTERED_STATUSES.has(status.registrationStatus);
157
+ }
158
+ export function isSimCardOk(status) {
159
+ if (!status.simCardStatus?.trim())
160
+ return true;
161
+ return OK_SIM_CARD_STATUSES.has(status.simCardStatus.trim().toUpperCase());
162
+ }
163
+ export function isCellularServerCommunicationOk(status) {
164
+ if (status.legacySession) {
165
+ return status.communicationStatus != null && OK_COMM_STATUSES.has(status.communicationStatus);
166
+ }
167
+ return (status.serverCommunicationStatus != null &&
168
+ OK_COMM_STATUSES.has(status.serverCommunicationStatus));
169
+ }
170
+ export function isCellularConnectionFullyOk(status) {
171
+ return (isValidCellularIpAddress(status.ipAddress) &&
172
+ isSimCardOk(status) &&
173
+ isCellularRegistrationOk(status) &&
174
+ isCellularServerCommunicationOk(status));
175
+ }
176
+ function rawServerStatus(status) {
177
+ const raw = status.legacySession ? status.communicationStatus : status.serverCommunicationStatus;
178
+ return raw?.trim() ? raw.trim() : null;
179
+ }
180
+ function normalizeCommKey(raw) {
181
+ return raw.toUpperCase().replace(/[\s-]+/g, '_');
182
+ }
183
+ function formatSimLabel(status, L) {
184
+ const raw = status.simCardStatus?.trim().toUpperCase();
185
+ if (raw === 'NOT_INSERTED' || raw === 'ABSENT')
186
+ return L.simMissing;
187
+ if (raw === 'PIN_REQUIRED')
188
+ return L.simPin;
189
+ return L.simError;
190
+ }
191
+ /** Server/cloud detail (Timeout, No server, …). */
192
+ export function formatCellularServerDetailLabel(status, locale = 'en') {
193
+ const L = labelsFor(locale);
194
+ const raw = rawServerStatus(status);
195
+ if (!raw)
196
+ return L.comm.UNKNOWN;
197
+ const key = normalizeCommKey(raw);
198
+ return L.comm[key] ?? raw;
199
+ }
200
+ /**
201
+ * Short status label for UI (No SIM, No internet, No server, Server OK).
202
+ * Same mapping as Tempivo live BLE strip (without operator).
203
+ */
204
+ export function formatCellularNetworkStatusLabel(status, locale = 'en') {
205
+ const L = labelsFor(locale);
206
+ if (!isSimCardOk(status))
207
+ return formatSimLabel(status, L);
208
+ if (!isValidCellularIpAddress(status.ipAddress))
209
+ return L.noInternet;
210
+ if (isCellularServerCommunicationOk(status) && isCellularRegistrationOk(status)) {
211
+ return L.serverOk;
212
+ }
213
+ if (!isCellularRegistrationOk(status))
214
+ return L.noNetwork;
215
+ return formatCellularServerDetailLabel(status, locale);
216
+ }
217
+ /** Prefix status with operator when GATT reports a registered PLMN. */
218
+ export function formatCellularNetworkStatusLabelWithOperator(status, locale = 'en') {
219
+ const statusLabel = formatCellularNetworkStatusLabel(status, locale);
220
+ const operatorName = getOperatorNameFromPlmn(status.registeredPlmn);
221
+ if (!operatorName)
222
+ return statusLabel;
223
+ return `${operatorName}, ${statusLabel}`;
224
+ }
225
+ /** One short sentence for toasts / logs. */
226
+ export function formatCellularNetworkStatusSummary(status, opts) {
227
+ const locale = opts?.locale ?? 'en';
228
+ const L = labelsFor(locale);
229
+ let body;
230
+ if (!isSimCardOk(status))
231
+ body = formatSimLabel(status, L);
232
+ else if (isCellularConnectionFullyOk(status))
233
+ body = L.statusOk;
234
+ else if (!isValidCellularIpAddress(status.ipAddress))
235
+ body = L.noInternet;
236
+ else if (isCellularRegistrationOk(status)) {
237
+ body = opts?.afterServerTrigger
238
+ ? L.statusCloudPending
239
+ : L.statusServer(formatCellularServerDetailLabel(status, locale));
240
+ }
241
+ else if (isValidCellularIpAddress(status.ipAddress))
242
+ body = L.statusUnregistered;
243
+ else
244
+ body = L.statusNoIp;
245
+ const operatorName = getOperatorNameFromPlmn(status.registeredPlmn);
246
+ if (!operatorName)
247
+ return body;
248
+ return `${operatorName}. ${body}`;
249
+ }
250
+ export function resolveCellularStatusTone(status) {
251
+ if (!isSimCardOk(status))
252
+ return 'bad';
253
+ if (!isValidCellularIpAddress(status.ipAddress))
254
+ return 'bad';
255
+ if (isCellularConnectionFullyOk(status))
256
+ return 'ok';
257
+ if (!isCellularRegistrationOk(status))
258
+ return 'bad';
259
+ return 'warn';
260
+ }
261
+ /** Build a partner-ready view from raw GATT JSON (no radio). */
262
+ export function cellularNetworkStatusViewFromJson(json, opts) {
263
+ const locale = opts?.locale ?? 'en';
264
+ const L = labelsFor(locale);
265
+ const status = parseCellularNetworkStatusJson(json, opts?.legacySession === true);
266
+ if (!status) {
267
+ return {
268
+ json,
269
+ status: null,
270
+ operatorPlmn: null,
271
+ operatorName: null,
272
+ label: L.unavailable,
273
+ statusLabel: L.unavailable,
274
+ summary: L.unavailable,
275
+ tone: 'muted',
276
+ };
277
+ }
278
+ const statusLabel = formatCellularNetworkStatusLabel(status, locale);
279
+ return {
280
+ json,
281
+ status,
282
+ operatorPlmn: status.registeredPlmn,
283
+ operatorName: getOperatorNameFromPlmn(status.registeredPlmn),
284
+ label: formatCellularNetworkStatusLabelWithOperator(status, locale),
285
+ statusLabel,
286
+ summary: formatCellularNetworkStatusSummary(status, {
287
+ locale,
288
+ afterServerTrigger: opts?.afterServerTrigger,
289
+ }),
290
+ tone: resolveCellularStatusTone(status),
291
+ };
292
+ }
@@ -0,0 +1,43 @@
1
+ import type { TempivoTemperatureAlert } from './session-types.js';
2
+ /** Partner probe alerts map to 1-based measurement channel 3 (0-based index 2). */
3
+ export declare const PROBE_CHANNEL_INDEX_0 = 2;
4
+ /** Normalize ProtoConfig channelTypes entry (int or enum string) to a type id. */
5
+ export declare function measurementTypeId(value: unknown): number | null;
6
+ /**
7
+ * True when `channelTypes` includes measurement channel 3 (0-based index 2).
8
+ * Ambient+RH only is typically length 2 → false. Probe / dual layouts are length ≥ 3.
9
+ */
10
+ export declare function deviceSupportsProbeFromChannelTypes(channelTypes: unknown): boolean;
11
+ /** Parse `0x01` / `01` / `temperature` style labels from scan telemetry. */
12
+ export declare function isTemperatureReadingType(typeHex: unknown): boolean;
13
+ /**
14
+ * Infer probe from beacon scan readings.
15
+ * @returns `true` / `false` when readings are present; `null` if unknown (no readings yet).
16
+ */
17
+ export declare function deviceSupportsProbeFromBeaconReadings(readings: ReadonlyArray<{
18
+ typeHex?: string | null;
19
+ }> | null | undefined): boolean | null;
20
+ export declare function alertsRequireProbe(alerts: ReadonlyArray<Pick<TempivoTemperatureAlert, 'channel'>> | null | undefined): boolean;
21
+ export type ProbeCapabilitySource = {
22
+ kind: 'channelTypes';
23
+ channelTypes: unknown;
24
+ } | {
25
+ kind: 'beaconReadings';
26
+ readings: ReadonlyArray<{
27
+ typeHex?: string | null;
28
+ }>;
29
+ } | {
30
+ kind: 'unknown';
31
+ };
32
+ /**
33
+ * Resolve whether probe alerts are allowed.
34
+ * `channelTypes` wins when present; else beacon readings; else unknown (do not block).
35
+ */
36
+ export declare function resolveProbeSupported(source: ProbeCapabilitySource): boolean | null;
37
+ /**
38
+ * Throw {@link TempivoSensorError} `invalidConfig` when probe alerts target a device without ch3.
39
+ * No-op when capability is unknown (missing channelTypes / empty scan).
40
+ */
41
+ export declare function assertProbeAlertsAllowed(alerts: ReadonlyArray<Pick<TempivoTemperatureAlert, 'channel'>> | null | undefined, source: ProbeCapabilitySource): void;
42
+ /** Extract channelTypes from a decoded ProtoConfig object. */
43
+ export declare function channelTypesFromProtoConfig(cfg: unknown): unknown[] | null;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Detect whether the device has a probe (measurement channel 3) so partner
3
+ * `temperatureAlerts` with `channel: "probe"` are not written to ambient-only sensors.
4
+ *
5
+ * Sources (prefer in order):
6
+ * 1. ProtoConfig `channelTypes` from `getConfiguration` baseline (always available before set)
7
+ * 2. BLE scan-response readings (frame `0x04`) — count temperature slots
8
+ */
9
+ import { TempivoSensorError } from './errors.js';
10
+ /** Efento Proto measurement type: temperature. */
11
+ const MEASUREMENT_TYPE_TEMPERATURE = 1;
12
+ /** Partner probe alerts map to 1-based measurement channel 3 (0-based index 2). */
13
+ export const PROBE_CHANNEL_INDEX_0 = 2;
14
+ function isRecord(v) {
15
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
16
+ }
17
+ /** Normalize ProtoConfig channelTypes entry (int or enum string) to a type id. */
18
+ export function measurementTypeId(value) {
19
+ if (typeof value === 'number' && Number.isFinite(value))
20
+ return value;
21
+ if (typeof value === 'string') {
22
+ const s = value.trim();
23
+ if (!s)
24
+ return null;
25
+ if (/^\d+$/.test(s))
26
+ return Number(s);
27
+ if (/TEMPERATURE/i.test(s))
28
+ return MEASUREMENT_TYPE_TEMPERATURE;
29
+ // Unknown enum name: treat as present non-temp when checking length only.
30
+ return -1;
31
+ }
32
+ return null;
33
+ }
34
+ /**
35
+ * True when `channelTypes` includes measurement channel 3 (0-based index 2).
36
+ * Ambient+RH only is typically length 2 → false. Probe / dual layouts are length ≥ 3.
37
+ */
38
+ export function deviceSupportsProbeFromChannelTypes(channelTypes) {
39
+ if (!Array.isArray(channelTypes) || channelTypes.length <= PROBE_CHANNEL_INDEX_0) {
40
+ return false;
41
+ }
42
+ // Prefer a real temperature on ch3; any allocated slot still means the channel exists.
43
+ const probeType = measurementTypeId(channelTypes[PROBE_CHANNEL_INDEX_0]);
44
+ return probeType != null;
45
+ }
46
+ /** Parse `0x01` / `01` / `temperature` style labels from scan telemetry. */
47
+ export function isTemperatureReadingType(typeHex) {
48
+ if (typeof typeHex !== 'string')
49
+ return false;
50
+ const s = typeHex.trim().toLowerCase();
51
+ if (s === 'temperature' || s.includes('temperature'))
52
+ return true;
53
+ const m = s.match(/^0x([0-9a-f]+)$/i) || s.match(/^([0-9a-f]+)$/i);
54
+ if (!m)
55
+ return false;
56
+ return parseInt(m[1], 16) === MEASUREMENT_TYPE_TEMPERATURE;
57
+ }
58
+ /**
59
+ * Infer probe from beacon scan readings.
60
+ * @returns `true` / `false` when readings are present; `null` if unknown (no readings yet).
61
+ */
62
+ export function deviceSupportsProbeFromBeaconReadings(readings) {
63
+ if (!Array.isArray(readings) || readings.length === 0)
64
+ return null;
65
+ const tempCount = readings.filter((r) => isTemperatureReadingType(r?.typeHex)).length;
66
+ return tempCount >= 2;
67
+ }
68
+ export function alertsRequireProbe(alerts) {
69
+ return (alerts ?? []).some((a) => a.channel === 'probe');
70
+ }
71
+ /**
72
+ * Resolve whether probe alerts are allowed.
73
+ * `channelTypes` wins when present; else beacon readings; else unknown (do not block).
74
+ */
75
+ export function resolveProbeSupported(source) {
76
+ if (source.kind === 'channelTypes') {
77
+ if (!Array.isArray(source.channelTypes))
78
+ return null;
79
+ return deviceSupportsProbeFromChannelTypes(source.channelTypes);
80
+ }
81
+ if (source.kind === 'beaconReadings') {
82
+ return deviceSupportsProbeFromBeaconReadings(source.readings);
83
+ }
84
+ return null;
85
+ }
86
+ /**
87
+ * Throw {@link TempivoSensorError} `invalidConfig` when probe alerts target a device without ch3.
88
+ * No-op when capability is unknown (missing channelTypes / empty scan).
89
+ */
90
+ export function assertProbeAlertsAllowed(alerts, source) {
91
+ if (!alertsRequireProbe(alerts))
92
+ return;
93
+ const supported = resolveProbeSupported(source);
94
+ if (supported === null)
95
+ return;
96
+ if (supported)
97
+ return;
98
+ throw new TempivoSensorError('invalidConfig', 'temperatureAlerts use channel "probe" but this device has no probe measurement channel (only ambient). Use channel "ambient" or a sensor with an external probe.');
99
+ }
100
+ /** Extract channelTypes from a decoded ProtoConfig object. */
101
+ export function channelTypesFromProtoConfig(cfg) {
102
+ if (!isRecord(cfg))
103
+ return null;
104
+ const raw = cfg.channelTypes ?? cfg.channel_types;
105
+ if (!Array.isArray(raw) || raw.length === 0)
106
+ return null;
107
+ return raw;
108
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Decode ProtoConfig.configurationErrors (uint32 codes) from BLE get.
3
+ * Wire: (side << 14) | (parameter << 8) | value
4
+ */
5
+ export type TempivoConfigErrorSide = 'BLE' | 'SERVER' | 'INTERNAL' | 'UNRECOGNIZED';
6
+ export type TempivoConfigErrorParameter = 'RULE0' | 'RULE1' | 'RULE2' | 'RULE3' | 'RULE4' | 'RULE5' | 'RULE6' | 'RULE7' | 'RULE8' | 'RULE9' | 'RULE10' | 'RULE11' | 'CACHE' | 'MEASUREMENT_PERIOD' | 'BLE_TURNOFF_TIME' | 'TRANSFER_LIMIT' | 'TRANSMISSION_INTERVAL' | 'DATA_SERVER_CONFIG' | 'UPDATE_SERVER_CONFIG' | 'CLOUD_TOKEN' | 'SUPERVISION_PERIOD' | 'DEVICE_INFO_ENDPOINT' | 'TIME_ENDPOINT' | 'BLUETOOTH_TX_POWER' | 'TIME_SYNCHRONIZATION_CONFIG' | 'DNS_TTL_CONFIG' | 'CELLULAR_CONFIG' | 'RULE_CALENDAR_0' | 'RULE_CALENDAR_1' | 'RULE_CALENDAR_2' | 'RULE_CALENDAR_3' | 'RULE_CALENDAR_4' | 'RULE_CALENDAR_5' | 'LED' | 'NETWORK' | 'ENCRYPTION' | 'CONFIG_PAYLOAD_OR_REQUEST' | 'DATA_SENDING_ALGORITHM' | 'OUTPUT_CONTROL' | 'UNRECOGNIZED';
7
+ export type TempivoConfigErrorValue = 'NO_ERROR' | 'VALUE_OUT_OF_RANGE' | 'IMPOSSIBLE_TO_CONNECT_TO_THE_SET_SERVER' | 'INVALID_IP_ADDRESS' | 'INVALID_APN' | 'INVALID_PLMN' | 'INVALID_BANDS' | 'INVALID_CLOUD_TOKEN_VALUE' | 'INVALID_CLOUD_TOKEN_CONFIG' | 'INVALID_CONDITION' | 'INVALID_ACTION' | 'INVALID_SLOTS' | 'INVALID_PARAMS' | 'TIMEOUT' | 'INVALID_COAP_ENDPOINT' | 'FAILED_TO_DECODE_PROTOBUF_PAYLOAD' | 'INCORRECT_SENSOR_BEHAVIOR_NO_MEASUREMENTS_AT_PLANNED_TRANSMISSION_OVER_NB_IOT' | 'INVALID_CLOUD_TOKEN_COAP_OPTION' | 'INVALID_PAYLOAD_SIGNATURE_COAP_OPTION' | 'INVALID_DNS_SERVER_IP_ADDRESS' | 'INVALID_COAP_PORT' | 'INVALID_UDP_PORT' | 'INVALID_CHANNEL_CONTROL_REQUEST' | 'INVALID_RULE_MASK' | 'INVALID_CALIBRATION_PARAMETERS_REQUEST' | 'PREVIOUS_CONFIGURATION_IS_IN_PROGRESS' | 'UNRECOGNIZED';
8
+ export type TempivoConfigurationError = {
9
+ raw: number;
10
+ side: TempivoConfigErrorSide;
11
+ parameter: TempivoConfigErrorParameter;
12
+ value: TempivoConfigErrorValue;
13
+ /** Short label for logs / UI. */
14
+ label: string;
15
+ };
16
+ export type TempivoConfigurationErrors = {
17
+ errors: TempivoConfigurationError[];
18
+ /** ProtoConfig.configurationErrorTimestamp (unix seconds), or null when unset. */
19
+ errorTimestampUnix: number | null;
20
+ /** Same instant as `YYYY-MM-DD HH:mm:ss UTC`, or null when unset. */
21
+ errorTimestampUtc: string | null;
22
+ };
23
+ export declare function decodeConfigurationError(raw: number): TempivoConfigurationError;
24
+ export declare function decodeConfigurationErrorCodes(rawCodes: number[]): TempivoConfigurationError[];
25
+ /**
26
+ * Format ProtoConfig.configurationErrorTimestamp as `YYYY-MM-DD HH:mm:ss UTC`.
27
+ * Accepts unix seconds (number/string) or ISO / loader UTC strings.
28
+ */
29
+ export declare function formatConfigurationErrorTimestamp(value: unknown): string | null;
30
+ /** Parse `configurationErrors` + timestamp from a decoded ProtoConfig object. */
31
+ export declare function configurationErrorsFromProtoConfig(proto: Record<string, unknown>): TempivoConfigurationErrors;