@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 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
- <!-- Publish trigger: 1.1.0 not yet on npm until CI Trusted Publisher (ci.yml) runs. -->
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` | 60–600. Omitted: leave device value. | Returns current device value (may be below 60 if set outside the SDK). |
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: 60 min sampling → max `3600` s uplink; 120 min → max `7200` s.
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 **two** slots (breach + return). Up to **4** such ranges, or more min/max alerts if you disable `transmitOnReturn` on ranges.
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 the same OR layout as POST /devices/config-profiles.
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) ?: continue
35
- val t = alert.minC ?: continue
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) ?: continue
42
- val t = alert.maxC ?: continue
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 ?: continue
49
- val high = alert.highC ?: continue
50
- val pair = findPair(used) ?: continue
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
- if (ret) returnLowSlots.add(pair)
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) return
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 always = days.size == 7 && weekly.fromTime == 0 && weekly.toTime >= 1439
241
- return if (always) {
242
- TempivoSchedule(always = true)
243
- } else {
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 = 60.0
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 (1 hour).")
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.")
@@ -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
- return typeof config === 'string' ? config : JSON.stringify(config);
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 = 60;
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
- /** Same 12-slot layout as Cellular API, including one extra slot per range with transmitOnReturn. */
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
- returnLowSlots.push(pair);
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
- for (const _low of returnLowSlots) {
66
- const slot = findFreeSlot(used);
67
- if (slot === null) {
68
- throw new TempivoSensorError('invalidConfig', 'Not enough free alarm rule slots for transmit-on-return (OR logic).');
69
- }
70
- used[slot] = true;
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
- return { weekdays: days, from, to, utcOffsetMinutes: utc };
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 = 60;
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} (1 hour).`);
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-simulator</string>
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>
@@ -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 = 60
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)) (1 hour).")
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.0",
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": {