@tempivo/sensor-beacon 1.1.0 → 1.1.2
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.
- package/CHANGELOG.md +19 -1
- package/README.md +13 -5
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +64 -42
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +9 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +30 -2
- package/dist/native-helpers.js +3 -1
- package/dist/profile.d.ts +1 -1
- package/dist/profile.js +35 -15
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +4 -4
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,7 +6,25 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## [1.1.2] - 2026-09-05
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- BLE get: `schedule: { always: true }` no longer reads back as `weekdays: [6]`.
|
|
14
|
+
- BLE set: dual ambient + probe ranges with `transmitOnReturn` keep alerts on get (interleaved OR after each pair).
|
|
15
|
+
- Native compile fail-closed on bad/missing alert fields or full slots (no silent drop).
|
|
16
|
+
- Post-set readback requires alert count ≥ written count.
|
|
17
|
+
- iOS parse aligned with Android/TS (required thresholds, schedule fields, slot budget).
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- README: range + `transmitOnReturn` uses **3** slots; weekday BLE get caveat.
|
|
22
|
+
|
|
23
|
+
## [1.1.1] - 2026-09-03
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Partner write min for `measurementIntervalMinutes` is **1** (was 60). Transmission min remains **3600** s.
|
|
10
28
|
|
|
11
29
|
## [1.1.0] - 2026-09-02
|
|
12
30
|
|
package/README.md
CHANGED
|
@@ -74,7 +74,11 @@ await setConfigurationJson({
|
|
|
74
74
|
},
|
|
75
75
|
],
|
|
76
76
|
});
|
|
77
|
+
// BLE write is on-device only. Cloud pending / last report update after a cellular uplink:
|
|
77
78
|
await triggerTransmission();
|
|
79
|
+
// Tempivo then drops from the cloud queue only fields the device already matches;
|
|
80
|
+
// other queued Cellular fields can still downlink. To cancel the whole queue:
|
|
81
|
+
// DELETE /api/v1/devices/{serial}/config (see docs/cellular-api.md).
|
|
78
82
|
const applied = await getConfiguration();
|
|
79
83
|
const cal = await getCalibration();
|
|
80
84
|
await disconnect();
|
|
@@ -100,9 +104,11 @@ The Expo config plugin sets **minSdk 26**, Android BLE permissions, and **`NSBlu
|
|
|
100
104
|
| `addDeviceFoundListener` | Callback per discovered device (includes `telemetry` when decoded). |
|
|
101
105
|
| `connect` / `disconnect` | GATT session with parsed QR (`TempivoSensorQr`). |
|
|
102
106
|
| `getConfiguration` / `setConfigurationJson` | Read / write alert rules, schedule, optional intervals. |
|
|
103
|
-
| `triggerTransmission` | Ask firmware to uplink now; returns `{ ok }`. |
|
|
107
|
+
| `triggerTransmission` | Ask firmware to uplink now; returns `{ ok }`. Cloud pending syncs after that uplink (or schedule/button), not from the BLE write alone. |
|
|
104
108
|
| `getCalibration` | Laboratory calibration date from extended config. |
|
|
105
109
|
|
|
110
|
+
**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).
|
|
111
|
+
|
|
106
112
|
### QR + connect helpers
|
|
107
113
|
|
|
108
114
|
Shared with the Tempivo app (same sticker JSON, PIN → encryption key, serial match checks):
|
|
@@ -250,14 +256,16 @@ Ignored over BLE: `slug`, `name`. Do not pass `alarmRules`.
|
|
|
250
256
|
|
|
251
257
|
| Field | Write (`setConfigurationJson`) | Read (`getConfiguration`) |
|
|
252
258
|
|------|--------------------------------|---------------------------|
|
|
253
|
-
| `measurementIntervalMinutes` |
|
|
259
|
+
| `measurementIntervalMinutes` | 1–600. Omitted: leave device value. | Returns current device value. |
|
|
254
260
|
| `transmissionIntervalSeconds` | 3600–604800 (1 h–7 d). Omitted: leave device value. | Returns current device value. |
|
|
255
261
|
|
|
256
|
-
When **both** are set on write, uplink must be at most **60×** the measurement interval. Example:
|
|
262
|
+
When **both** are set on write, uplink must be at most **60×** the measurement interval. Example: 1 min sampling → max `3600` s uplink; 60 min → max `3600` s; 120 min → max `7200` s.
|
|
257
263
|
|
|
258
264
|
Use `parseSensorConfigurationJson` to validate before write. Use `parseSensorConfigurationFromDevice` / `parseSensorConfigurationFromDeviceJson` when parsing a **readback** from the device (relaxed interval rules). For `GET /devices/config-profiles/{slug}`, use `configurationFromApiProfile(response.profile)` or pass the `{ profile }` envelope directly to `setConfigurationJson`.
|
|
259
265
|
|
|
260
|
-
**Alert slot budget:** the device has **12** rule slots. A `range` with default `transmitOnReturn: true` uses **
|
|
266
|
+
**Alert slot budget:** the device has **12** rule slots. A `range` with default `transmitOnReturn: true` uses **three** slots (`low` + `high` + OR). Up to **4** such ranges. A lone `min`/`max` uses one; with return on a range disabled, a range uses two.
|
|
267
|
+
|
|
268
|
+
**Weekday schedules** (`Mon–Fri`, etc.): BLE **get** may not round-trip weekdays on current partner firmware (day-mask decode). Prefer `{ "always": true }` when possible. After set, if get looks wrong (e.g. only Sunday), trust what you wrote.
|
|
261
269
|
|
|
262
270
|
**Each `temperatureAlerts[]` item**
|
|
263
271
|
|
|
@@ -372,7 +380,7 @@ AAR after `npm run build:android` (from `packages/tempivo-sensor-beacon`):
|
|
|
372
380
|
|
|
373
381
|
Sources: `android-aar/library`. Request `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT`, and on some OEMs **`ACCESS_FINE_LOCATION`** even on API 31+ (location on older APIs).
|
|
374
382
|
|
|
375
|
-
**Scan:** temperature and other live values are in scan-response frame **`0x04`** (`telemetry.readings`). Frame **`0x03`** alone fills firmware / battery / cellular but leaves `readings` empty until an active scan delivers `0x04` (usually within a second or two). Keep scanning; do not stop on the first event.
|
|
383
|
+
**Scan:** temperature and other live values are in scan-response frame **`0x04`** (`telemetry.readings`). Frame **`0x03`** alone fills firmware / battery / cellular but leaves `readings` empty until an active scan delivers `0x04` (usually within a second or two). Keep scanning; do not stop on the first event. Regression coverage: `src/scan-telemetry.regression.test.ts`.
|
|
376
384
|
|
|
377
385
|
```kotlin
|
|
378
386
|
val qr = TempivoSensorQrParser.parse(stickerQrText)
|
|
@@ -16,7 +16,8 @@ import pl.efento.mobile.bluetooth.model.sensor.TriggeringMode
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Compile Cellular API `temperatureAlerts` + `schedule` into device rules.
|
|
19
|
-
* Range + `transmitOnReturn` uses
|
|
19
|
+
* Range + `transmitOnReturn` uses an OR slot immediately after each low/high pair
|
|
20
|
+
* (same layout as a single range with return; required for dual-range BLE writes).
|
|
20
21
|
*/
|
|
21
22
|
internal object PartnerBleRules {
|
|
22
23
|
fun compile(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> {
|
|
@@ -24,30 +25,30 @@ internal object PartnerBleRules {
|
|
|
24
25
|
Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION)
|
|
25
26
|
}
|
|
26
27
|
val used = BooleanArray(12)
|
|
27
|
-
val returnLowSlots = mutableListOf<Int>()
|
|
28
28
|
|
|
29
29
|
for (alert in cfg.temperatureAlerts) {
|
|
30
30
|
val channel = if (alert.channel == "probe") 3 else 1
|
|
31
31
|
val hyst = alert.hysteresisC ?: 1.0
|
|
32
32
|
when (alert.type) {
|
|
33
33
|
"min" -> {
|
|
34
|
-
val slot = findFree(used) ?:
|
|
35
|
-
val t = alert.minC ?:
|
|
34
|
+
val slot = findFree(used) ?: throw compileFail("Not enough free alarm rule slots.")
|
|
35
|
+
val t = alert.minC ?: throw compileFail("min alert is missing minC.")
|
|
36
36
|
val action = if (alert.transmitOnBreach) Action.TRIGGER_TRANSMISSION_WITH_ACK else Action.NO_ACTION
|
|
37
37
|
putThreshold(rules, slot, channel, low = true, t, hyst, action)
|
|
38
38
|
used[slot] = true
|
|
39
39
|
}
|
|
40
40
|
"max" -> {
|
|
41
|
-
val slot = findFree(used) ?:
|
|
42
|
-
val t = alert.maxC ?:
|
|
41
|
+
val slot = findFree(used) ?: throw compileFail("Not enough free alarm rule slots.")
|
|
42
|
+
val t = alert.maxC ?: throw compileFail("max alert is missing maxC.")
|
|
43
43
|
val action = if (alert.transmitOnBreach) Action.TRIGGER_TRANSMISSION_WITH_ACK else Action.NO_ACTION
|
|
44
44
|
putThreshold(rules, slot, channel, low = false, t, hyst, action)
|
|
45
45
|
used[slot] = true
|
|
46
46
|
}
|
|
47
47
|
"range" -> {
|
|
48
|
-
val low = alert.lowC ?:
|
|
49
|
-
val high = alert.highC ?:
|
|
50
|
-
val pair = findPair(used)
|
|
48
|
+
val low = alert.lowC ?: throw compileFail("range alert is missing lowC.")
|
|
49
|
+
val high = alert.highC ?: throw compileFail("range alert is missing highC.")
|
|
50
|
+
val pair = findPair(used)
|
|
51
|
+
?: throw compileFail("Not enough free alarm rule slots for another range (need two consecutive slots).")
|
|
51
52
|
val ret = alert.transmitOnReturn
|
|
52
53
|
val action =
|
|
53
54
|
when {
|
|
@@ -59,31 +60,19 @@ internal object PartnerBleRules {
|
|
|
59
60
|
putThreshold(rules, pair + 1, channel, low = false, high, hyst, action)
|
|
60
61
|
used[pair] = true
|
|
61
62
|
used[pair + 1] = true
|
|
62
|
-
|
|
63
|
+
// Place OR immediately after each pair (low, high, OR, …).
|
|
64
|
+
// Batching all pairs then ORs at the end clears dual-range configs on device.
|
|
65
|
+
if (ret) {
|
|
66
|
+
val logicSlot = findFree(used)
|
|
67
|
+
?: throw compileFail("Not enough free alarm rule slots for transmit-on-return (OR logic).")
|
|
68
|
+
putReturnOr(rules, logicSlot, pair)
|
|
69
|
+
used[logicSlot] = true
|
|
70
|
+
}
|
|
63
71
|
}
|
|
72
|
+
else -> throw compileFail("Unsupported temperature alert type: ${alert.type}.")
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
|
|
67
|
-
for (lowSlot in returnLowSlots) {
|
|
68
|
-
val logicSlot = findFree(used) ?: break
|
|
69
|
-
rules[logicSlot] =
|
|
70
|
-
Rule(
|
|
71
|
-
logicSlot + 1,
|
|
72
|
-
emptySet(),
|
|
73
|
-
LogicOperatorCondition(
|
|
74
|
-
LogicOperatorConditionParameters(
|
|
75
|
-
LogicOperator.OR,
|
|
76
|
-
setOf(lowSlot + 1, lowSlot + 2),
|
|
77
|
-
emptySet(),
|
|
78
|
-
0,
|
|
79
|
-
0,
|
|
80
|
-
),
|
|
81
|
-
),
|
|
82
|
-
Action.TRIGGER_TRANSMISSION_WITH_ACK,
|
|
83
|
-
)
|
|
84
|
-
used[logicSlot] = true
|
|
85
|
-
}
|
|
86
|
-
|
|
87
76
|
val active = rules.filter { it.condition !is DisabledCondition }.map { it.id }.toSet()
|
|
88
77
|
val calendars = MutableList(6) { i ->
|
|
89
78
|
RuleCalendar(i + 1, emptySet(), RuleCalendar.Type.Disabled)
|
|
@@ -129,6 +118,17 @@ internal object PartnerBleRules {
|
|
|
129
118
|
)
|
|
130
119
|
used[i] = true
|
|
131
120
|
used[i + 1] = true
|
|
121
|
+
if (hasReturn) {
|
|
122
|
+
for (j in 0 until 12) {
|
|
123
|
+
val c = padded[j].condition as? LogicOperatorCondition ?: continue
|
|
124
|
+
val p = c.conditionParameters
|
|
125
|
+
if (p.logicOperator != LogicOperator.OR) continue
|
|
126
|
+
if (p.selectedRules == setOf(i + 1, i + 2)) {
|
|
127
|
+
used[j] = true
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
for (i in 0 until 12) {
|
|
@@ -200,6 +200,9 @@ internal object PartnerBleRules {
|
|
|
200
200
|
else -> null
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
private fun compileFail(message: String): Nothing =
|
|
204
|
+
throw TempivoSensorException(TempivoSensorException.Code.INVALID_CONFIG, message)
|
|
205
|
+
|
|
203
206
|
private fun findPair(used: BooleanArray): Int? {
|
|
204
207
|
for (i in 0..10) {
|
|
205
208
|
if (!used[i] && !used[i + 1]) return i
|
|
@@ -221,7 +224,7 @@ internal object PartnerBleRules {
|
|
|
221
224
|
hyst: Double,
|
|
222
225
|
action: Action,
|
|
223
226
|
) {
|
|
224
|
-
if (slot !in 0..11)
|
|
227
|
+
if (slot !in 0..11) throw compileFail("Alarm rule slot out of range: $slot.")
|
|
225
228
|
val params =
|
|
226
229
|
TemperatureLowHighThresholdConditionParameters(
|
|
227
230
|
threshold,
|
|
@@ -233,22 +236,41 @@ internal object PartnerBleRules {
|
|
|
233
236
|
rules[slot] = Rule(slot + 1, setOf(channel), condition, action)
|
|
234
237
|
}
|
|
235
238
|
|
|
239
|
+
private fun putReturnOr(rules: MutableList<Rule>, logicSlot: Int, lowSlot: Int) {
|
|
240
|
+
rules[logicSlot] =
|
|
241
|
+
Rule(
|
|
242
|
+
logicSlot + 1,
|
|
243
|
+
emptySet(),
|
|
244
|
+
LogicOperatorCondition(
|
|
245
|
+
LogicOperatorConditionParameters(
|
|
246
|
+
LogicOperator.OR,
|
|
247
|
+
setOf(lowSlot + 1, lowSlot + 2),
|
|
248
|
+
emptySet(),
|
|
249
|
+
0,
|
|
250
|
+
0,
|
|
251
|
+
),
|
|
252
|
+
),
|
|
253
|
+
Action.TRIGGER_TRANSMISSION_WITH_ACK,
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
236
257
|
private fun decompileSchedule(calendars: List<RuleCalendar>?): TempivoSchedule {
|
|
237
258
|
val weekly = calendars.orEmpty().map { it.type }.filterIsInstance<RuleCalendar.Type.Weekly>().firstOrNull()
|
|
238
259
|
?: return TempivoSchedule(always = true)
|
|
239
260
|
val days = weekly.days.map { dayToPartnerIndex(it) }.sorted()
|
|
240
|
-
val
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
TempivoSchedule(
|
|
245
|
-
always = false,
|
|
246
|
-
weekdays = days,
|
|
247
|
-
from = TempivoSensorProfile.hhmmFromMinutes(weekly.fromTime),
|
|
248
|
-
to = TempivoSensorProfile.hhmmFromMinutes(weekly.toTime),
|
|
249
|
-
utcOffsetMinutes = weekly.timezoneOffset,
|
|
250
|
-
)
|
|
261
|
+
val fullDay = weekly.fromTime == 0 && weekly.toTime >= 1439
|
|
262
|
+
// Efento BLE SDK day-mask decode keeps only Sunday (`(mask & bit) != 1`).
|
|
263
|
+
// always:true (all days, 00:00–23:59) therefore reads back as weekdays [6].
|
|
264
|
+
if (fullDay && (days.size == 7 || days.isEmpty() || days == listOf(6))) {
|
|
265
|
+
return TempivoSchedule(always = true)
|
|
251
266
|
}
|
|
267
|
+
return TempivoSchedule(
|
|
268
|
+
always = false,
|
|
269
|
+
weekdays = days,
|
|
270
|
+
from = TempivoSensorProfile.hhmmFromMinutes(weekly.fromTime),
|
|
271
|
+
to = TempivoSensorProfile.hhmmFromMinutes(weekly.toTime),
|
|
272
|
+
utcOffsetMinutes = weekly.timezoneOffset,
|
|
273
|
+
)
|
|
252
274
|
}
|
|
253
275
|
|
|
254
276
|
private fun weeklyType(schedule: TempivoSchedule): RuleCalendar.Type {
|
|
@@ -88,6 +88,15 @@ internal object SensorBleRuntime {
|
|
|
88
88
|
next = next.copy(transmissionInterval = TransmissionInterval(sec))
|
|
89
89
|
}
|
|
90
90
|
session.connection.commands.setConfiguration(next) { }
|
|
91
|
+
if (cfg.temperatureAlerts.isNotEmpty()) {
|
|
92
|
+
val verify = decompile(modernGetConfiguration(session.connection))
|
|
93
|
+
if (verify.temperatureAlerts.size < cfg.temperatureAlerts.size) {
|
|
94
|
+
throw TempivoSensorException(
|
|
95
|
+
TempivoSensorException.Code.UNKNOWN,
|
|
96
|
+
"Device rejected alarm rules (readback ${verify.temperatureAlerts.size}/${cfg.temperatureAlerts.size}).",
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
suspend fun triggerTransmission(session: Session): TempivoSensorSession.TriggerResult {
|
|
@@ -32,7 +32,7 @@ data class TempivoSensorConfiguration(
|
|
|
32
32
|
|
|
33
33
|
object TempivoSensorProfile {
|
|
34
34
|
private val timeRe = Regex("^([01]\\d|2[0-3]):([0-5]\\d)$")
|
|
35
|
-
private const val MIN_MEASUREMENT_MINUTES =
|
|
35
|
+
private const val MIN_MEASUREMENT_MINUTES = 1.0
|
|
36
36
|
private const val MAX_MEASUREMENT_MINUTES = 600.0
|
|
37
37
|
private const val MIN_TRANSMISSION_SECONDS = 3600
|
|
38
38
|
private const val MAX_TRANSMISSION_SECONDS = 604_800
|
|
@@ -68,6 +68,7 @@ object TempivoSensorProfile {
|
|
|
68
68
|
val item = arr.optJSONObject(i) ?: throw invalid("temperatureAlerts[$i] must be an object.")
|
|
69
69
|
alerts.add(parseAlert(item, i))
|
|
70
70
|
}
|
|
71
|
+
assertAlertSlotBudget(alerts)
|
|
71
72
|
val intervals = parseOptionalIntervals(body)
|
|
72
73
|
return TempivoSensorConfiguration(
|
|
73
74
|
alerts,
|
|
@@ -111,6 +112,33 @@ object TempivoSensorProfile {
|
|
|
111
112
|
return root.toString()
|
|
112
113
|
}
|
|
113
114
|
|
|
115
|
+
|
|
116
|
+
private fun assertAlertSlotBudget(alerts: List<TempivoTemperatureAlert>) {
|
|
117
|
+
val used = BooleanArray(12)
|
|
118
|
+
fun findPair(): Int? {
|
|
119
|
+
for (i in 0..10) if (!used[i] && !used[i + 1]) return i
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
fun findFree(): Int? {
|
|
123
|
+
for (i in 0 until 12) if (!used[i]) return i
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
for (alert in alerts) {
|
|
127
|
+
if (alert.type == "range") {
|
|
128
|
+
val pair = findPair() ?: throw invalid("Not enough free alarm rule slots for another range (need two consecutive slots).")
|
|
129
|
+
used[pair] = true
|
|
130
|
+
used[pair + 1] = true
|
|
131
|
+
if (alert.transmitOnReturn) {
|
|
132
|
+
val logic = findFree() ?: throw invalid("Not enough free alarm rule slots for transmit-on-return (OR logic).")
|
|
133
|
+
used[logic] = true
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
val slot = findFree() ?: throw invalid("Not enough free alarm rule slots.")
|
|
137
|
+
used[slot] = true
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
private fun parseOptionalIntervals(body: JSONObject): Pair<Double?, Int?> {
|
|
115
143
|
var measurement: Double? = null
|
|
116
144
|
var transmission: Int? = null
|
|
@@ -120,7 +148,7 @@ object TempivoSensorProfile {
|
|
|
120
148
|
throw invalid("measurementIntervalMinutes must be a positive number.")
|
|
121
149
|
}
|
|
122
150
|
if (n < MIN_MEASUREMENT_MINUTES) {
|
|
123
|
-
throw invalid("measurementIntervalMinutes must be at least $MIN_MEASUREMENT_MINUTES
|
|
151
|
+
throw invalid("measurementIntervalMinutes must be at least $MIN_MEASUREMENT_MINUTES.")
|
|
124
152
|
}
|
|
125
153
|
if (n > MAX_MEASUREMENT_MINUTES) {
|
|
126
154
|
throw invalid("measurementIntervalMinutes must be at most $MAX_MEASUREMENT_MINUTES.")
|
package/dist/native-helpers.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TempivoSensorError } from './errors.js';
|
|
2
|
+
import { pickPartnerConfigurationJson } from './profile.js';
|
|
2
3
|
const SENSOR_CODES = [
|
|
3
4
|
'invalidPin',
|
|
4
5
|
'invalidQr',
|
|
@@ -22,7 +23,8 @@ export function qrToConnectJson(qr) {
|
|
|
22
23
|
return JSON.stringify(body);
|
|
23
24
|
}
|
|
24
25
|
export function configToJson(config) {
|
|
25
|
-
|
|
26
|
+
// Always validate partner shape + slot budget before native setConfiguration.
|
|
27
|
+
return pickPartnerConfigurationJson(typeof config === 'string' ? config : JSON.stringify(config));
|
|
26
28
|
}
|
|
27
29
|
function isSensorCode(value) {
|
|
28
30
|
return !!value && SENSOR_CODES.includes(value);
|
package/dist/profile.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { TempivoSensorConfiguration } from './session-types.js';
|
|
|
2
2
|
/** Same default as Cellular API config profiles. */
|
|
3
3
|
export declare const DEFAULT_ALERT_HYSTERESIS_C = 1;
|
|
4
4
|
/** Same limits as Cellular API simple profiles. */
|
|
5
|
-
export declare const BLE_MIN_MEASUREMENT_INTERVAL_MINUTES =
|
|
5
|
+
export declare const BLE_MIN_MEASUREMENT_INTERVAL_MINUTES = 1;
|
|
6
6
|
export declare const BLE_MAX_MEASUREMENT_INTERVAL_MINUTES = 600;
|
|
7
7
|
export declare const BLE_MIN_TRANSMISSION_INTERVAL_SECONDS = 3600;
|
|
8
8
|
export declare const BLE_MAX_TRANSMISSION_INTERVAL_SECONDS = 604800;
|
package/dist/profile.js
CHANGED
|
@@ -39,10 +39,12 @@ function findFreeSlot(used) {
|
|
|
39
39
|
}
|
|
40
40
|
return null;
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* 12-slot BLE layout: each range with transmitOnReturn uses low+high then an OR
|
|
44
|
+
* slot immediately after (same order native compile writes).
|
|
45
|
+
*/
|
|
43
46
|
function assertAlertSlotBudget(alerts) {
|
|
44
47
|
const used = Array(12).fill(false);
|
|
45
|
-
const returnLowSlots = [];
|
|
46
48
|
for (const alert of alerts) {
|
|
47
49
|
if (alert.type === 'range') {
|
|
48
50
|
const pair = findPairStart(used);
|
|
@@ -51,8 +53,13 @@ function assertAlertSlotBudget(alerts) {
|
|
|
51
53
|
}
|
|
52
54
|
used[pair] = true;
|
|
53
55
|
used[pair + 1] = true;
|
|
54
|
-
if (alert.transmitOnReturn)
|
|
55
|
-
|
|
56
|
+
if (alert.transmitOnReturn) {
|
|
57
|
+
const logic = findFreeSlot(used);
|
|
58
|
+
if (logic === null) {
|
|
59
|
+
throw new TempivoSensorError('invalidConfig', 'Not enough free alarm rule slots for transmit-on-return (OR logic).');
|
|
60
|
+
}
|
|
61
|
+
used[logic] = true;
|
|
62
|
+
}
|
|
56
63
|
}
|
|
57
64
|
else {
|
|
58
65
|
const slot = findFreeSlot(used);
|
|
@@ -62,15 +69,27 @@ function assertAlertSlotBudget(alerts) {
|
|
|
62
69
|
used[slot] = true;
|
|
63
70
|
}
|
|
64
71
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Efento BLE SDK (2.3/2.4) day-mask decode compares `(mask & bit) != 1`, so only
|
|
75
|
+
* Sunday (bit 0) survives. Writing `schedule: { always: true }` (all 7 days,
|
|
76
|
+
* 00:00–23:59) therefore reads back as `weekdays: [6]`. Normalize that shape on
|
|
77
|
+
* device read so get matches set.
|
|
78
|
+
*/
|
|
79
|
+
function normalizeScheduleFromDevice(schedule) {
|
|
80
|
+
if ('always' in schedule && schedule.always)
|
|
81
|
+
return schedule;
|
|
82
|
+
if (!('weekdays' in schedule))
|
|
83
|
+
return schedule;
|
|
84
|
+
if (schedule.from !== '00:00' || schedule.to !== '23:59')
|
|
85
|
+
return schedule;
|
|
86
|
+
const days = [...new Set(schedule.weekdays)].sort((a, b) => a - b);
|
|
87
|
+
if (days.length === 7 || days.length === 0 || (days.length === 1 && days[0] === 6)) {
|
|
88
|
+
return { always: true };
|
|
71
89
|
}
|
|
90
|
+
return schedule;
|
|
72
91
|
}
|
|
73
|
-
function parseSchedule(raw) {
|
|
92
|
+
function parseSchedule(raw, fromDevice = false) {
|
|
74
93
|
if (raw == null)
|
|
75
94
|
return { always: true };
|
|
76
95
|
if (!isRecord(raw)) {
|
|
@@ -99,7 +118,8 @@ function parseSchedule(raw) {
|
|
|
99
118
|
if (utc === undefined || !Number.isInteger(utc)) {
|
|
100
119
|
throw new TempivoSensorError('invalidConfig', 'schedule.utcOffsetMinutes is required.');
|
|
101
120
|
}
|
|
102
|
-
|
|
121
|
+
const schedule = { weekdays: days, from, to, utcOffsetMinutes: utc };
|
|
122
|
+
return fromDevice ? normalizeScheduleFromDevice(schedule) : schedule;
|
|
103
123
|
}
|
|
104
124
|
function parseAlert(item, index) {
|
|
105
125
|
if (!isRecord(item)) {
|
|
@@ -191,7 +211,7 @@ function rejectWireAlarmRules(rec) {
|
|
|
191
211
|
}
|
|
192
212
|
}
|
|
193
213
|
/** Same limits as Cellular API simple profiles. */
|
|
194
|
-
export const BLE_MIN_MEASUREMENT_INTERVAL_MINUTES =
|
|
214
|
+
export const BLE_MIN_MEASUREMENT_INTERVAL_MINUTES = 1;
|
|
195
215
|
export const BLE_MAX_MEASUREMENT_INTERVAL_MINUTES = 600;
|
|
196
216
|
export const BLE_MIN_TRANSMISSION_INTERVAL_SECONDS = 3600;
|
|
197
217
|
export const BLE_MAX_TRANSMISSION_INTERVAL_SECONDS = 604_800;
|
|
@@ -205,7 +225,7 @@ function parseOptionalIntervals(body, options = {}) {
|
|
|
205
225
|
}
|
|
206
226
|
if (validatePartnerLimits) {
|
|
207
227
|
if (n < BLE_MIN_MEASUREMENT_INTERVAL_MINUTES) {
|
|
208
|
-
throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at least ${BLE_MIN_MEASUREMENT_INTERVAL_MINUTES}
|
|
228
|
+
throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at least ${BLE_MIN_MEASUREMENT_INTERVAL_MINUTES}.`);
|
|
209
229
|
}
|
|
210
230
|
if (n > BLE_MAX_MEASUREMENT_INTERVAL_MINUTES) {
|
|
211
231
|
throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at most ${BLE_MAX_MEASUREMENT_INTERVAL_MINUTES}.`);
|
|
@@ -249,7 +269,7 @@ function parseSensorConfigurationBody(body, options = {}) {
|
|
|
249
269
|
const intervals = parseOptionalIntervals(body, options);
|
|
250
270
|
return {
|
|
251
271
|
temperatureAlerts,
|
|
252
|
-
schedule: parseSchedule(body.schedule),
|
|
272
|
+
schedule: parseSchedule(body.schedule, options.fromDevice === true),
|
|
253
273
|
...intervals,
|
|
254
274
|
};
|
|
255
275
|
}
|
|
Binary file
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<key>BinaryPath</key>
|
|
9
9
|
<string>TempivoSensorBridge.framework/TempivoSensorBridge</string>
|
|
10
10
|
<key>LibraryIdentifier</key>
|
|
11
|
-
<string>ios-arm64
|
|
11
|
+
<string>ios-arm64</string>
|
|
12
12
|
<key>LibraryPath</key>
|
|
13
13
|
<string>TempivoSensorBridge.framework</string>
|
|
14
14
|
<key>SupportedArchitectures</key>
|
|
@@ -17,14 +17,12 @@
|
|
|
17
17
|
</array>
|
|
18
18
|
<key>SupportedPlatform</key>
|
|
19
19
|
<string>ios</string>
|
|
20
|
-
<key>SupportedPlatformVariant</key>
|
|
21
|
-
<string>simulator</string>
|
|
22
20
|
</dict>
|
|
23
21
|
<dict>
|
|
24
22
|
<key>BinaryPath</key>
|
|
25
23
|
<string>TempivoSensorBridge.framework/TempivoSensorBridge</string>
|
|
26
24
|
<key>LibraryIdentifier</key>
|
|
27
|
-
<string>ios-arm64</string>
|
|
25
|
+
<string>ios-arm64-simulator</string>
|
|
28
26
|
<key>LibraryPath</key>
|
|
29
27
|
<string>TempivoSensorBridge.framework</string>
|
|
30
28
|
<key>SupportedArchitectures</key>
|
|
@@ -33,6 +31,8 @@
|
|
|
33
31
|
</array>
|
|
34
32
|
<key>SupportedPlatform</key>
|
|
35
33
|
<string>ios</string>
|
|
34
|
+
<key>SupportedPlatformVariant</key>
|
|
35
|
+
<string>simulator</string>
|
|
36
36
|
</dict>
|
|
37
37
|
</array>
|
|
38
38
|
<key>CFBundlePackageType</key>
|
|
Binary file
|
|
@@ -90,7 +90,7 @@ public enum TempivoSensorProfile {
|
|
|
90
90
|
return String(data: data, encoding: .utf8) ?? "{}"
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
private static let minMeasurementMinutes: Double =
|
|
93
|
+
private static let minMeasurementMinutes: Double = 1
|
|
94
94
|
private static let maxMeasurementMinutes: Double = 600
|
|
95
95
|
private static let minTransmissionSeconds = 3600
|
|
96
96
|
private static let maxTransmissionSeconds = 604_800
|
|
@@ -107,7 +107,7 @@ public enum TempivoSensorProfile {
|
|
|
107
107
|
}
|
|
108
108
|
if validatePartnerLimits {
|
|
109
109
|
if n < minMeasurementMinutes {
|
|
110
|
-
throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at least \(Int(minMeasurementMinutes))
|
|
110
|
+
throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at least \(Int(minMeasurementMinutes)).")
|
|
111
111
|
}
|
|
112
112
|
if n > maxMeasurementMinutes {
|
|
113
113
|
throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at most \(Int(maxMeasurementMinutes)).")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tempivo/sensor-beacon",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.2",
|
|
4
4
|
"description": "Tempivo sensor BLE SDK. Native scan and GATT for Expo, React Native, Android, and iOS. Node helpers decode advertisements and parse QR or config (no radio).",
|
|
5
5
|
"homepage": "https://app.tempivo.com/integrations",
|
|
6
6
|
"repository": {
|