@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
package/CHANGELOG.md CHANGED
@@ -6,6 +6,29 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.1.9] - 2026-10-14
10
+
11
+ ### Changed
12
+
13
+ - npm package metadata: drop private GitHub `repository` so npmjs.com no longer links Changelog / bugs to an inaccessible repo. Changelog remains in the published package (`CHANGELOG.md`).
14
+
15
+ ## [1.1.8] - 2026-10-14
16
+
17
+ ### Added
18
+
19
+ - Cellular (HC7 native): `getCellularNetworkStatus()` returns short partner labels (`label`, `statusLabel`, `operatorName`, `summary`, `tone`; `locale`: `en` \| `sv`), same vocabulary as the Tempivo app (No SIM, No internet, No server, Server OK, Telia Sweden, …). Raw GATT JSON via `getCellularNetworkStatusJson()`.
20
+ - Pure helpers (no radio): `cellularNetworkStatusViewFromJson`, `parseCellularNetworkStatusJson`, `formatCellularNetworkStatusLabel`, `getOperatorNameFromPlmn`, and related exports.
21
+ - `setOperator('AUTO' | country ISO)` sets preferred PLMN (e.g. `SE` → 24001).
22
+ - `getConfigurationErrors()`: read `configurationErrors` (+ timestamp) from ProtoConfig over BLE after set/get. Helpers: `decodeConfigurationError`, `configurationErrorsFromProtoConfig`, `formatConfigurationErrorTimestamp`. Human `label` matches cloud Slack (`side · parameter · value`); `errorTimestampUtc` is `YYYY-MM-DD HH:mm:ss UTC`.
23
+ - Reject `temperatureAlerts` with `channel: "probe"` when the device baseline `channelTypes` has no measurement channel 3 (ambient-only sensors). Uses ProtoConfig from `getConfiguration` before BLE set. Helpers: `deviceSupportsProbeFromChannelTypes`, `deviceSupportsProbeFromBeaconReadings`, `assertProbeAlertsAllowed`.
24
+
25
+ ### Changed
26
+
27
+ - Production defaults: treat all-zero UUID cloud tokens and CoAP paths without a leading `/` (`m` vs `/m`) as matching factory settings.
28
+ - Production defaults: measurement period and transmission interval accept any positive value; `networkTroubleshooting=2` is OK when BLE turn-off is never.
29
+ - BLE `getCalibration()` / `decodeLaboratoryCalibrationTimestamp`: normalize timestamp to unix seconds and expose calendar date only (`laboratoryCalibrationDate`). Helpers: `formatLaboratoryCalibration`, `normalizeLaboratoryCalibration`. No wall-clock time (device stores date only).
30
+ - Auto-release leftover GATT on JS module load and before `startScan` (Metro / process reuse). Still call `disconnect()` when you finish a session.
31
+
9
32
  ## [1.1.7] - 2026-09-30
10
33
 
11
34
  ### Added
package/README.md CHANGED
@@ -49,6 +49,7 @@ import {
49
49
  connect,
50
50
  setConfigurationJson,
51
51
  getConfiguration,
52
+ getConfigurationErrors,
52
53
  triggerTransmission,
53
54
  disconnect,
54
55
  } from '@tempivo/sensor-beacon';
@@ -81,6 +82,9 @@ try {
81
82
  // other queued Cellular fields can still downlink. To cancel the whole queue:
82
83
  // DELETE /api/v1/devices/{serial}/config (see docs/cellular-api.md).
83
84
  const applied = await getConfiguration();
85
+ // After set, firmware may store reject codes in ProtoConfig.configurationErrors:
86
+ const { errors, errorTimestampUnix, errorTimestampUtc } = await getConfigurationErrors();
87
+ if (errors.length) console.warn('config errors', errors, errorTimestampUtc ?? errorTimestampUnix);
84
88
  } finally {
85
89
  await disconnect(); // required so advertising / startScan can see the sensor again
86
90
  }
@@ -90,7 +94,7 @@ sub.remove();
90
94
 
91
95
  `startScan` and `connect` request Bluetooth permission. Sticker PIN is required on `connect`. Wrong PIN is `invalidPin`.
92
96
 
93
- **Connect tips:** stop scan before GATT connect. Pass the scan row’s `deviceId` and MAC when they differ from the sticker serial (`parseSensorQrForConnect`). 5–6 digit PINs map to an encryption key internally; numeric `resetCode` alone is not enough on all devices. Call `disconnect()` when finished. **While any device holds a GATT connection, the sensor does not advertise**, so `startScan` will not show beacon / live readings for that sensor until everyone disconnects (including other apps).
97
+ **Connect tips:** stop scan before GATT connect. Pass the scan row’s `deviceId` and MAC when they differ from the sticker serial (`parseSensorQrForConnect`). 5–6 digit PINs map to an encryption key internally; numeric `resetCode` alone is not enough on all devices. Call `disconnect()` when finished. The SDK also releases a leftover GATT session on JS module load and before `startScan` (Metro reload / process reuse often keeps native BLE alive while wiping JS). **While any device holds a GATT connection, the sensor does not advertise**, so `startScan` will not show beacon / live readings for that sensor until everyone disconnects (including other apps).
94
98
 
95
99
  If a bundler misses native BLE, import `@tempivo/sensor-beacon/react-native`.
96
100
 
@@ -106,9 +110,23 @@ The Expo config plugin sets **minSdk 26**, Android BLE permissions, and **`NSBlu
106
110
  | `addDeviceFoundListener` | Callback per discovered device (includes `telemetry` when decoded). |
107
111
  | `connect` / `disconnect` | GATT session with parsed QR (`TempivoSensorQr`). |
108
112
  | `getConfiguration` / `setConfigurationJson` | Read / write alert rules, schedule, optional intervals. |
113
+ | `getConfigurationErrors` | Read decoded `configurationErrors` from ProtoConfig (BLE get). Empty list when none. |
109
114
  | `triggerTransmission` | Ask firmware to uplink now; returns `{ ok }`. Cloud pending syncs after that uplink (or schedule/button), not from the BLE write alone. |
110
- | `clearMeasurementQueue` | Clear on-device measurement / uplink queue (memory reset). Manual recovery only; not used by `setConfigurationJson`. |
111
- | `getCalibration` | Optional BLE read of laboratory calibration date. Prefer the Cellular API (see below). |
115
+ | `clearMeasurementQueue` / `deleteMeasurements` | Clear on-device measurement / uplink queue (memory reset). Manual recovery only; not used by `setConfigurationJson`. |
116
+ | `getCellularNetworkStatus` / `getCellularNetworkStatusJson` / `setOperator` | GATT network status with short labels (`label` includes operator when known, plus `statusLabel` / `operatorName` / `summary` / `tone`; `locale`: `en` \| `sv`), raw JSON, PLMN `AUTO` or country ISO (`SE`, …). |
117
+ | `cellularNetworkStatusViewFromJson` (+ related) | Pure label helpers from stored GATT JSON (no radio). |
118
+ | `getCalibration` | BLE read of lab calibration. Returns `laboratoryCalibrationDate` (`YYYY-MM-DD`) and unix `laboratoryCalibrationTimestamp` (start of that day). Device has no wall-clock time. Prefer Cellular API in production. |
119
+
120
+ ```ts
121
+ const { label, statusLabel, operatorName, summary, tone } = await getCellularNetworkStatus({
122
+ locale: 'en',
123
+ });
124
+ // label: "Telia Sweden, Server OK" (operator from GATT rplmn when known)
125
+ // statusLabel: "No server" | "No internet" | "Server OK" | …
126
+ // operatorName: "Telia Sweden" | null
127
+
128
+ await setOperator('AUTO'); // or 'SE', 'NO', …
129
+ ```
112
130
 
113
131
  **Cloud queue:** BLE updates the sensor only. After uplink, Tempivo drops from Cellular `pendingConfig` only fields the device already matches; other queued fields can still downlink. Cancel the whole queue with `DELETE /api/v1/devices/{serial}/config` (see Tempivo Cellular API docs).
114
132
 
@@ -281,7 +299,7 @@ Use `parseSensorConfigurationJson` to validate before write. Use `parseSensorCon
281
299
  | Field | Meaning |
282
300
  |------|---------|
283
301
  | `type` | `range` (band), `min` (below), or `max` (above). |
284
- | `channel` | `ambient` or `probe`. |
302
+ | `channel` | `ambient` or `probe`. **Probe** requires measurement channel 3 on the device (external probe). Ambient-only sensors (temp + humidity) reject probe alerts before write when `getConfiguration` baseline `channelTypes` is known. You can also check scan `telemetry.readings` via `deviceSupportsProbeFromBeaconReadings`. |
285
303
  | `lowC` / `highC` | Inclusive band for `type: "range"`. |
286
304
  | `minC` | Threshold for `type: "min"`. |
287
305
  | `maxC` | Threshold for `type: "max"`. |
@@ -458,7 +476,7 @@ npm run test:unit
458
476
 
459
477
  ## Changelog
460
478
 
461
- See [CHANGELOG.md](./CHANGELOG.md).
479
+ See [CHANGELOG.md](./CHANGELOG.md) in this package (also on npm under Code).
462
480
 
463
481
  ## License
464
482
 
@@ -36,6 +36,9 @@ final class SensorBeaconNative {
36
36
 
37
37
  void startScan(Context context, DeviceCallback callback) {
38
38
  try {
39
+ // Own GATT stops advertising for that sensor; release before scan (Metro/app relaunch
40
+ // often leaves a live session while JS is wiped).
41
+ disconnect();
39
42
  requireScanner(context).startScan(device -> callback.onDevice(deviceMap(device)));
40
43
  } catch (Throwable error) {
41
44
  throw wrap(error);
@@ -133,6 +136,22 @@ final class SensorBeaconNative {
133
136
  }
134
137
  }
135
138
 
139
+ String getCellularNetworkStatusJson() {
140
+ try {
141
+ return requireSessionConnected().getCellularNetworkStatusJsonBlocking();
142
+ } catch (Throwable error) {
143
+ throw wrap(error);
144
+ }
145
+ }
146
+
147
+ void setOperator(boolean automatic, Integer plmn) {
148
+ try {
149
+ requireSessionConnected().setOperatorBlocking(automatic, plmn);
150
+ } catch (Throwable error) {
151
+ throw wrap(error);
152
+ }
153
+ }
154
+
136
155
  Map<String, Object> getCalibration() {
137
156
  try {
138
157
  return calibrationMap(requireSessionConnected().getCalibrationBlocking());
@@ -100,6 +100,22 @@ class TempivoSensorBeaconModule : Module() {
100
100
  }
101
101
  }
102
102
 
103
+ AsyncFunction("getCellularNetworkStatusJson") {
104
+ try {
105
+ native.getCellularNetworkStatusJson()
106
+ } catch (error: Throwable) {
107
+ throw error.toSensorException()
108
+ }
109
+ }
110
+
111
+ AsyncFunction("setOperator") { automatic: Boolean, plmn: Int? ->
112
+ try {
113
+ native.setOperator(automatic, plmn)
114
+ } catch (error: Throwable) {
115
+ throw error.toSensorException()
116
+ }
117
+ }
118
+
103
119
  AsyncFunction("getCalibration") {
104
120
  try {
105
121
  native.calibration
@@ -30,6 +30,7 @@ dependencies {
30
30
  implementation "androidx.core:core-ktx:1.17.0"
31
31
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
32
32
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
33
+ implementation "com.google.code.gson:gson:2.11.0"
33
34
  implementation "pl.efento.mobile:bluetooth:2.3.0"
34
35
  testImplementation "junit:junit:4.13.2"
35
36
  }
@@ -9,8 +9,10 @@ import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
9
9
  import pl.efento.mobile.bluetooth.model.MeasurementPeriod
10
10
  import pl.efento.mobile.bluetooth.model.sensor.Rule
11
11
  import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
12
+ import pl.efento.mobile.bluetooth.model.Plmn
12
13
  import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
13
14
  import pl.efento.mobile.bluetooth.model.sensor.cellular.TransmissionInterval
15
+ import com.google.gson.Gson
14
16
 
15
17
  /**
16
18
  * Internal adapter to the sensor BLE runtime. Keep vendor types inside this file.
@@ -38,8 +40,14 @@ internal object SensorBleRuntime {
38
40
  TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.", e)
39
41
  className.contains("NotConnected") ->
40
42
  TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.", e)
41
- else ->
42
- TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Sensor request failed.", e)
43
+ else -> {
44
+ val detail = e.message?.takeIf { it.isNotBlank() } ?: e.javaClass.simpleName
45
+ TempivoSensorException(
46
+ TempivoSensorException.Code.UNKNOWN,
47
+ "Sensor request failed ($detail).",
48
+ e,
49
+ )
50
+ }
43
51
  }
44
52
  }
45
53
 
@@ -120,12 +128,73 @@ internal object SensorBleRuntime {
120
128
  }
121
129
 
122
130
  /**
123
- * Clear on-device measurement / uplink queue (memory reset).
131
+ * Clear on-device measurement / uplink queue (memory reset / delete measurements).
124
132
  * Manual recovery only: not used by [writeConfiguration] / partner setConfiguration.
125
133
  */
126
134
  suspend fun clearMeasurementQueue(session: Session) {
127
135
  val base = modernGetConfiguration(session.connection)
128
- session.connection.commands.setConfiguration(base.copy(resetMemoryRequest = true)) { }
136
+ val next =
137
+ prepareForSetConfiguration(
138
+ base.copy(
139
+ resetMemoryRequest = true,
140
+ updateSoftwareRequest = false,
141
+ clearRuntimeErrorsRequest = false,
142
+ configurationRequest = false,
143
+ deviceInfoRequest = false,
144
+ extendedConfigurationRequest = false,
145
+ ),
146
+ )
147
+ session.connection.commands.setConfiguration(next) { }
148
+ }
149
+
150
+ suspend fun getCellularNetworkStatusJson(session: Session): String {
151
+ val status = session.connection.commands.getCellularNetworkStatus { }
152
+ return Gson().toJson(status)
153
+ }
154
+
155
+ /**
156
+ * Set PLMN AUTO or CUSTOM (operator code). Bypasses partner setConfigurationJson cellular block.
157
+ *
158
+ * Merges into a fresh getConfiguration read (same as Tempivo Capacitor) and truncates rules
159
+ * so SDK 2.3 does not reject oversized readbacks. Stays on the vendor GATT session (no raw
160
+ * ProtoConfig link, which drops the session and can hang the Expo UI).
161
+ */
162
+ suspend fun setOperator(session: Session, automatic: Boolean, plmn: Int?) {
163
+ val plmnValue =
164
+ if (automatic) {
165
+ Plmn.Automatic
166
+ } else {
167
+ require(plmn != null && plmn in 10_000..999_999) { "plmn required for VALUE mode" }
168
+ Plmn.Custom(plmn)
169
+ }
170
+ val base = modernGetConfiguration(session.connection)
171
+ val next =
172
+ prepareForSetConfiguration(
173
+ base.copy(
174
+ plmn = plmnValue,
175
+ updateSoftwareRequest = false,
176
+ resetMemoryRequest = false,
177
+ clearRuntimeErrorsRequest = false,
178
+ configurationRequest = false,
179
+ deviceInfoRequest = false,
180
+ extendedConfigurationRequest = false,
181
+ ),
182
+ )
183
+ session.connection.commands.setConfiguration(next) { }
184
+ }
185
+
186
+ /** Truncate rule slots SDK 2.3 rejects on setConfiguration (>12 rules / >6 calendars). */
187
+ private fun prepareForSetConfiguration(cfg: SensorConfiguration): SensorConfiguration {
188
+ var out = cfg
189
+ val rules = out.rules
190
+ if (rules != null && rules.size > 12) {
191
+ out = out.copy(rules = rules.take(12))
192
+ }
193
+ val calendars = out.ruleCalendars
194
+ if (calendars != null && calendars.size > 6) {
195
+ out = out.copy(ruleCalendars = calendars.take(6))
196
+ }
197
+ return out
129
198
  }
130
199
 
131
200
  suspend fun readCalibration(session: Session): TempivoSensorCalibration {
@@ -6,6 +6,7 @@ import java.util.TimeZone
6
6
 
7
7
  data class TempivoSensorCalibration(
8
8
  val laboratoryCalibrationDate: String?,
9
+ /** Unix seconds UTC at start of that day (days-since-epoch from device are converted). */
9
10
  val laboratoryCalibrationTimestamp: Long?,
10
11
  )
11
12
 
@@ -16,16 +17,11 @@ object TempivoSensorCalibrationDecoder {
16
17
  if (raw == null || raw <= 0L) {
17
18
  return TempivoSensorCalibration(null, null)
18
19
  }
19
- val date =
20
- if (raw < 100_000L) {
21
- isoDateUtc(raw * SECONDS_PER_DAY)
22
- } else {
23
- isoDateUtc(raw)
24
- }
25
- return TempivoSensorCalibration(date, raw)
20
+ val unixSeconds = if (raw < 100_000L) raw * SECONDS_PER_DAY else raw
21
+ return TempivoSensorCalibration(utcDate(unixSeconds), unixSeconds)
26
22
  }
27
23
 
28
- private fun isoDateUtc(unixSeconds: Long): String {
24
+ private fun utcDate(unixSeconds: Long): String {
29
25
  val cal = Calendar.getInstance(TimeZone.getTimeZone("UTC"), Locale.US)
30
26
  cal.timeInMillis = unixSeconds * 1000L
31
27
  val y = cal.get(Calendar.YEAR)
@@ -180,6 +180,21 @@ class TempivoSensorSession(
180
180
  }
181
181
  }
182
182
 
183
+ suspend fun getCellularNetworkStatusJson(): String {
184
+ return runGatt {
185
+ ensureSdkConnected()
186
+ SensorBleRuntime.getCellularNetworkStatusJson(requireSession())
187
+ }
188
+ }
189
+
190
+ /** @param automatic true for AUTO; otherwise [plmn] required. */
191
+ suspend fun setOperator(automatic: Boolean, plmn: Int? = null) {
192
+ runGatt {
193
+ ensureSdkConnected()
194
+ SensorBleRuntime.setOperator(requireSession(), automatic, plmn)
195
+ }
196
+ }
197
+
183
198
  suspend fun getCalibration(): TempivoSensorCalibration {
184
199
  return runGatt {
185
200
  ensureSdkConnected()
@@ -204,6 +219,10 @@ class TempivoSensorSession(
204
219
 
205
220
  fun clearMeasurementQueueBlocking() = runBlocking { clearMeasurementQueue() }
206
221
 
222
+ fun getCellularNetworkStatusJsonBlocking(): String = runBlocking { getCellularNetworkStatusJson() }
223
+
224
+ fun setOperatorBlocking(automatic: Boolean, plmn: Int?) = runBlocking { setOperator(automatic, plmn) }
225
+
207
226
  fun getCalibrationBlocking(): TempivoSensorCalibration = runBlocking { getCalibration() }
208
227
 
209
228
  private fun requireSession(): SensorBleRuntime.Session =
@@ -2,6 +2,14 @@ import type { TempivoSensorCalibration } from './session-types.js';
2
2
  /**
3
3
  * Device extended config may send lab date as Unix seconds or as days since 1970.
4
4
  * Values below 100_000 are treated as days (year 2243 in seconds, year 2243 in days is far larger).
5
+ * Always returns unix seconds in {@link TempivoSensorCalibration.laboratoryCalibrationTimestamp}.
6
+ * Device stores a calendar date only (no wall-clock time).
5
7
  */
6
8
  export declare function decodeLaboratoryCalibrationTimestamp(raw: number | null | undefined): TempivoSensorCalibration;
9
+ /** One-line label for logs / UI (`YYYY-MM-DD` or `none`). */
10
+ export declare function formatLaboratoryCalibration(cal: TempivoSensorCalibration): string;
11
+ /**
12
+ * Normalize a native/partner calibration payload (date and/or timestamp) into the full shape.
13
+ */
14
+ export declare function normalizeLaboratoryCalibration(input: Partial<TempivoSensorCalibration> | null | undefined): TempivoSensorCalibration;
7
15
  export declare function calibrationFromExtendedJson(json: string): TempivoSensorCalibration;
@@ -2,33 +2,68 @@ const SECONDS_PER_DAY = 86_400;
2
2
  function pad2(n) {
3
3
  return n < 10 ? `0${n}` : String(n);
4
4
  }
5
- function isoDateUtcFromUnixSeconds(seconds) {
5
+ function utcDateFromUnixSeconds(seconds) {
6
6
  const d = new Date(seconds * 1000);
7
- return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
8
- }
9
- function isoDateUtcFromEpochDays(days) {
10
- return isoDateUtcFromUnixSeconds(days * SECONDS_PER_DAY);
7
+ const y = d.getUTCFullYear();
8
+ const m = pad2(d.getUTCMonth() + 1);
9
+ const day = pad2(d.getUTCDate());
10
+ return `${y}-${m}-${day}`;
11
11
  }
12
12
  /**
13
13
  * Device extended config may send lab date as Unix seconds or as days since 1970.
14
14
  * Values below 100_000 are treated as days (year 2243 in seconds, year 2243 in days is far larger).
15
+ * Always returns unix seconds in {@link TempivoSensorCalibration.laboratoryCalibrationTimestamp}.
16
+ * Device stores a calendar date only (no wall-clock time).
15
17
  */
16
18
  export function decodeLaboratoryCalibrationTimestamp(raw) {
17
19
  if (raw == null || !Number.isFinite(raw) || raw <= 0) {
18
- return { laboratoryCalibrationDate: null, laboratoryCalibrationTimestamp: null };
19
- }
20
- const n = Math.trunc(raw);
21
- if (n < 100_000) {
22
20
  return {
23
- laboratoryCalibrationDate: isoDateUtcFromEpochDays(n),
24
- laboratoryCalibrationTimestamp: n,
21
+ laboratoryCalibrationDate: null,
22
+ laboratoryCalibrationTimestamp: null,
25
23
  };
26
24
  }
25
+ const n = Math.trunc(raw);
26
+ const unixSeconds = n < 100_000 ? n * SECONDS_PER_DAY : n;
27
27
  return {
28
- laboratoryCalibrationDate: isoDateUtcFromUnixSeconds(n),
29
- laboratoryCalibrationTimestamp: n,
28
+ laboratoryCalibrationDate: utcDateFromUnixSeconds(unixSeconds),
29
+ laboratoryCalibrationTimestamp: unixSeconds,
30
30
  };
31
31
  }
32
+ /** One-line label for logs / UI (`YYYY-MM-DD` or `none`). */
33
+ export function formatLaboratoryCalibration(cal) {
34
+ return cal.laboratoryCalibrationDate ?? 'none';
35
+ }
36
+ /** Accept unix seconds from native (number or numeric string). */
37
+ function asPositiveUnix(value) {
38
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0)
39
+ return Math.trunc(value);
40
+ if (typeof value === 'string' && /^\d+$/.test(value.trim())) {
41
+ const n = parseInt(value.trim(), 10);
42
+ return n > 0 ? n : null;
43
+ }
44
+ return null;
45
+ }
46
+ /**
47
+ * Normalize a native/partner calibration payload (date and/or timestamp) into the full shape.
48
+ */
49
+ export function normalizeLaboratoryCalibration(input) {
50
+ if (!input || typeof input !== 'object') {
51
+ return decodeLaboratoryCalibrationTimestamp(null);
52
+ }
53
+ const fromTs = asPositiveUnix(input.laboratoryCalibrationTimestamp);
54
+ if (fromTs != null) {
55
+ return decodeLaboratoryCalibrationTimestamp(fromTs);
56
+ }
57
+ const date = typeof input.laboratoryCalibrationDate === 'string' && input.laboratoryCalibrationDate.trim()
58
+ ? input.laboratoryCalibrationDate.trim().slice(0, 10)
59
+ : null;
60
+ if (date && /^\d{4}-\d{2}-\d{2}$/.test(date)) {
61
+ const [y, m, d] = date.split('-').map((p) => parseInt(p, 10));
62
+ const unix = Math.floor(Date.UTC(y, m - 1, d) / 1000);
63
+ return decodeLaboratoryCalibrationTimestamp(unix);
64
+ }
65
+ return decodeLaboratoryCalibrationTimestamp(null);
66
+ }
32
67
  export function calibrationFromExtendedJson(json) {
33
68
  try {
34
69
  const o = JSON.parse(json);
@@ -37,6 +72,6 @@ export function calibrationFromExtendedJson(json) {
37
72
  return decodeLaboratoryCalibrationTimestamp(Number.isFinite(n) ? n : null);
38
73
  }
39
74
  catch {
40
- return { laboratoryCalibrationDate: null, laboratoryCalibrationTimestamp: null };
75
+ return decodeLaboratoryCalibrationTimestamp(null);
41
76
  }
42
77
  }
@@ -0,0 +1,69 @@
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
+ export type TempivoCellularNetworkStatus = {
6
+ ipAddress: string | null;
7
+ registrationStatus: string | null;
8
+ serverCommunicationStatus: string | null;
9
+ communicationStatus: string | null;
10
+ /** RSSI from GATT `signalStrength`, normalized to dBm when possible. */
11
+ signalStrengthDbm: number | null;
12
+ ecl: 0 | 1 | 2 | null;
13
+ rsrpDbm: number | null;
14
+ simCardStatus: string | null;
15
+ /** Registered / serving PLMN from GATT (`rplmn`). */
16
+ registeredPlmn: number | null;
17
+ legacySession: boolean;
18
+ };
19
+ export type TempivoCellularStatusLocale = 'en' | 'sv';
20
+ export type TempivoCellularStatusTone = 'ok' | 'warn' | 'bad' | 'muted';
21
+ export type TempivoCellularNetworkStatusView = {
22
+ /** Raw JSON from native GATT. */
23
+ json: string;
24
+ status: TempivoCellularNetworkStatus | null;
25
+ /** Registered PLMN from GATT (`rplmn`), when present. */
26
+ operatorPlmn: number | null;
27
+ /** Operator name (e.g. Tele2 Sweden), or `PLMN {code}` when unknown. */
28
+ operatorName: string | null;
29
+ /**
30
+ * Short chip label. Status only, or `Operator, status` when PLMN is known
31
+ * (e.g. Tele2 Sweden, No server).
32
+ */
33
+ label: string;
34
+ /** Status without operator (No internet, No server, Server OK). */
35
+ statusLabel: string;
36
+ /** One-line summary for toasts / logs (includes operator when known). */
37
+ summary: string;
38
+ tone: TempivoCellularStatusTone;
39
+ };
40
+ export declare function isValidCellularIpAddress(ip: string | null | undefined): boolean;
41
+ export declare function normalizeCellularSignalStrengthDbm(raw: number | null | undefined): number | null;
42
+ export declare function parseCellularNetworkStatusObject(obj: Record<string, unknown>, legacySession?: boolean): TempivoCellularNetworkStatus;
43
+ /** Parse Gson/SDK JSON from {@link getCellularNetworkStatusJson}. */
44
+ export declare function parseCellularNetworkStatusJson(json: string, legacySession?: boolean): TempivoCellularNetworkStatus | null;
45
+ export declare function isCellularRegistrationOk(status: TempivoCellularNetworkStatus): boolean;
46
+ export declare function isSimCardOk(status: TempivoCellularNetworkStatus): boolean;
47
+ export declare function isCellularServerCommunicationOk(status: TempivoCellularNetworkStatus): boolean;
48
+ export declare function isCellularConnectionFullyOk(status: TempivoCellularNetworkStatus): boolean;
49
+ /** Server/cloud detail (Timeout, No server, …). */
50
+ export declare function formatCellularServerDetailLabel(status: TempivoCellularNetworkStatus, locale?: TempivoCellularStatusLocale): string;
51
+ /**
52
+ * Short status label for UI (No SIM, No internet, No server, Server OK).
53
+ * Same mapping as Tempivo live BLE strip (without operator).
54
+ */
55
+ export declare function formatCellularNetworkStatusLabel(status: TempivoCellularNetworkStatus, locale?: TempivoCellularStatusLocale): string;
56
+ /** Prefix status with operator when GATT reports a registered PLMN. */
57
+ export declare function formatCellularNetworkStatusLabelWithOperator(status: TempivoCellularNetworkStatus, locale?: TempivoCellularStatusLocale): string;
58
+ /** One short sentence for toasts / logs. */
59
+ export declare function formatCellularNetworkStatusSummary(status: TempivoCellularNetworkStatus, opts?: {
60
+ locale?: TempivoCellularStatusLocale;
61
+ afterServerTrigger?: boolean;
62
+ }): string;
63
+ export declare function resolveCellularStatusTone(status: TempivoCellularNetworkStatus): TempivoCellularStatusTone;
64
+ /** Build a partner-ready view from raw GATT JSON (no radio). */
65
+ export declare function cellularNetworkStatusViewFromJson(json: string, opts?: {
66
+ locale?: TempivoCellularStatusLocale;
67
+ afterServerTrigger?: boolean;
68
+ legacySession?: boolean;
69
+ }): TempivoCellularNetworkStatusView;