@tempivo/sensor-beacon 1.1.1 → 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 +14 -0
- package/README.md +3 -1
- 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 +28 -0
- package/dist/native-helpers.js +3 -1
- package/dist/profile.js +33 -13
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
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
|
+
|
|
9
23
|
## [1.1.1] - 2026-09-03
|
|
10
24
|
|
|
11
25
|
### Changed
|
package/README.md
CHANGED
|
@@ -263,7 +263,9 @@ When **both** are set on write, uplink must be at most **60×** the measurement
|
|
|
263
263
|
|
|
264
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`.
|
|
265
265
|
|
|
266
|
-
**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.
|
|
267
269
|
|
|
268
270
|
**Each `temperatureAlerts[]` item**
|
|
269
271
|
|
|
@@ -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 {
|
|
@@ -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
|
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.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)) {
|
|
@@ -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
|
|
Binary file
|
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": {
|