@tempivo/sensor-beacon 0.1.0 → 0.2.0

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 (46) hide show
  1. package/README.md +209 -50
  2. package/android/build.gradle +4 -0
  3. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  4. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  5. package/android/gradle.properties +3 -0
  6. package/android/gradlew +251 -0
  7. package/android/library/build.gradle +30 -0
  8. package/android/library/consumer-rules.pro +1 -0
  9. package/android/library/src/main/AndroidManifest.xml +2 -0
  10. package/android/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +279 -0
  11. package/android/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +168 -0
  12. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +389 -0
  13. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +158 -0
  14. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +40 -0
  15. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +36 -0
  16. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +18 -0
  17. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +171 -0
  18. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +82 -0
  19. package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +114 -0
  20. package/android/settings.gradle +19 -0
  21. package/dist/calibration.d.ts +7 -0
  22. package/dist/calibration.js +42 -0
  23. package/dist/decoder.js +10 -18
  24. package/dist/errors.d.ts +6 -0
  25. package/dist/errors.js +8 -0
  26. package/dist/index.d.ts +6 -1
  27. package/dist/index.js +5 -1
  28. package/dist/profile.d.ts +10 -0
  29. package/dist/profile.js +226 -0
  30. package/dist/qr.d.ts +16 -0
  31. package/dist/qr.js +90 -0
  32. package/dist/session-types.d.ts +64 -0
  33. package/dist/session-types.js +1 -0
  34. package/dist/tempivo-sensor-beacon.aar +0 -0
  35. package/dist/types.d.ts +3 -3
  36. package/ios/Package.swift +22 -0
  37. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +409 -0
  38. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +159 -0
  39. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +103 -0
  40. package/ios/Sources/TempivoSensorBeacon/TempivoSensorCalibration.swift +22 -0
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +24 -0
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +123 -0
  43. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +78 -0
  44. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +161 -0
  45. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +93 -0
  46. package/package.json +20 -6
@@ -0,0 +1,279 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ import pl.efento.mobile.bluetooth.model.sensor.Action
4
+ import pl.efento.mobile.bluetooth.model.sensor.DisabledCondition
5
+ import pl.efento.mobile.bluetooth.model.sensor.HighThresholdCondition
6
+ import pl.efento.mobile.bluetooth.model.sensor.LogicOperator
7
+ import pl.efento.mobile.bluetooth.model.sensor.LogicOperatorCondition
8
+ import pl.efento.mobile.bluetooth.model.sensor.LogicOperatorConditionParameters
9
+ import pl.efento.mobile.bluetooth.model.sensor.LowThresholdCondition
10
+ import pl.efento.mobile.bluetooth.model.sensor.Rule
11
+ import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
12
+ import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
13
+ import pl.efento.mobile.bluetooth.model.sensor.TemperatureLowHighThresholdConditionParameters
14
+ import pl.efento.mobile.bluetooth.model.sensor.TriggeringMode
15
+
16
+ /**
17
+ * Compile Cellular API `temperatureAlerts` + `schedule` into device rules.
18
+ * Range + `transmitOnReturn` uses the same OR layout as POST /devices/config-profiles.
19
+ */
20
+ internal object PartnerBleRules {
21
+ fun compile(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> {
22
+ val rules = MutableList(12) { i ->
23
+ Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION)
24
+ }
25
+ val used = BooleanArray(12)
26
+ val returnLowSlots = mutableListOf<Int>()
27
+
28
+ for (alert in cfg.temperatureAlerts) {
29
+ val channel = if (alert.channel == "probe") 3 else 1
30
+ val hyst = alert.hysteresisC ?: 1.0
31
+ when (alert.type) {
32
+ "min" -> {
33
+ val slot = findFree(used) ?: continue
34
+ val t = alert.minC ?: continue
35
+ val action = if (alert.transmitOnBreach) Action.TRIGGER_TRANSMISSION_WITH_ACK else Action.NO_ACTION
36
+ putThreshold(rules, slot, channel, low = true, t, hyst, action)
37
+ used[slot] = true
38
+ }
39
+ "max" -> {
40
+ val slot = findFree(used) ?: continue
41
+ val t = alert.maxC ?: continue
42
+ val action = if (alert.transmitOnBreach) Action.TRIGGER_TRANSMISSION_WITH_ACK else Action.NO_ACTION
43
+ putThreshold(rules, slot, channel, low = false, t, hyst, action)
44
+ used[slot] = true
45
+ }
46
+ "range" -> {
47
+ val low = alert.lowC ?: continue
48
+ val high = alert.highC ?: continue
49
+ val pair = findPair(used) ?: continue
50
+ val ret = alert.transmitOnReturn
51
+ val action =
52
+ when {
53
+ ret -> Action.NO_ACTION
54
+ alert.transmitOnBreach -> Action.TRIGGER_TRANSMISSION_WITH_ACK
55
+ else -> Action.NO_ACTION
56
+ }
57
+ putThreshold(rules, pair, channel, low = true, low, hyst, action)
58
+ putThreshold(rules, pair + 1, channel, low = false, high, hyst, action)
59
+ used[pair] = true
60
+ used[pair + 1] = true
61
+ if (ret) returnLowSlots.add(pair)
62
+ }
63
+ }
64
+ }
65
+
66
+ for (lowSlot in returnLowSlots) {
67
+ val logicSlot = findFree(used) ?: break
68
+ rules[logicSlot] =
69
+ Rule(
70
+ logicSlot + 1,
71
+ emptySet(),
72
+ LogicOperatorCondition(
73
+ LogicOperatorConditionParameters(
74
+ LogicOperator.OR,
75
+ setOf(lowSlot + 1, lowSlot + 2),
76
+ emptySet(),
77
+ 0,
78
+ 0,
79
+ ),
80
+ ),
81
+ Action.TRIGGER_TRANSMISSION_WITH_ACK,
82
+ )
83
+ used[logicSlot] = true
84
+ }
85
+
86
+ val active = rules.filter { it.condition !is DisabledCondition }.map { it.id }.toSet()
87
+ val calendars = MutableList(6) { i ->
88
+ RuleCalendar(i + 1, emptySet(), RuleCalendar.Type.Disabled)
89
+ }
90
+ if (active.isNotEmpty()) {
91
+ calendars[0] = RuleCalendar(1, active, weeklyType(cfg.schedule))
92
+ }
93
+ return rules to calendars
94
+ }
95
+
96
+ fun decompile(cfg: SensorConfiguration): TempivoSensorConfiguration {
97
+ val padded = MutableList(12) { i ->
98
+ cfg.rules.orEmpty().getOrNull(i)
99
+ ?: Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION)
100
+ }
101
+ val orRefs = orSelectedPairs(padded)
102
+ val used = BooleanArray(12)
103
+ val alerts = mutableListOf<TempivoTemperatureAlert>()
104
+
105
+ for (i in 0..10) {
106
+ if (used[i] || used[i + 1]) continue
107
+ val low = padded[i]
108
+ val high = padded[i + 1]
109
+ if (low.condition !is LowThresholdCondition || high.condition !is HighThresholdCondition) continue
110
+ val lowCh = channelOf(low) ?: continue
111
+ val highCh = channelOf(high) ?: continue
112
+ if (lowCh != highCh) continue
113
+ val lowP = (low.condition as LowThresholdCondition).conditionParameters
114
+ val highP = (high.condition as HighThresholdCondition).conditionParameters
115
+ val hasReturn = orRefs.any { it.first == i + 1 && it.second == i + 2 }
116
+ val breachOnly =
117
+ !hasReturn && (low.action != Action.NO_ACTION || high.action != Action.NO_ACTION)
118
+ alerts.add(
119
+ TempivoTemperatureAlert(
120
+ type = "range",
121
+ channel = lowCh,
122
+ lowC = lowP.thresholdValue.toDouble(),
123
+ highC = highP.thresholdValue.toDouble(),
124
+ hysteresisC = lowP.hysteresisValue?.toDouble() ?: 1.0,
125
+ transmitOnBreach = hasReturn || breachOnly,
126
+ transmitOnReturn = hasReturn,
127
+ ),
128
+ )
129
+ used[i] = true
130
+ used[i + 1] = true
131
+ }
132
+
133
+ for (i in 0 until 12) {
134
+ if (used[i]) continue
135
+ val r = padded[i]
136
+ when (val c = r.condition) {
137
+ is LowThresholdCondition -> {
138
+ val ch = channelOf(r) ?: continue
139
+ alerts.add(
140
+ TempivoTemperatureAlert(
141
+ type = "min",
142
+ channel = ch,
143
+ minC = c.conditionParameters.thresholdValue.toDouble(),
144
+ hysteresisC = c.conditionParameters.hysteresisValue?.toDouble() ?: 1.0,
145
+ transmitOnBreach = r.action != Action.NO_ACTION,
146
+ ),
147
+ )
148
+ used[i] = true
149
+ }
150
+ is HighThresholdCondition -> {
151
+ val ch = channelOf(r) ?: continue
152
+ alerts.add(
153
+ TempivoTemperatureAlert(
154
+ type = "max",
155
+ channel = ch,
156
+ maxC = c.conditionParameters.thresholdValue.toDouble(),
157
+ hysteresisC = c.conditionParameters.hysteresisValue?.toDouble() ?: 1.0,
158
+ transmitOnBreach = r.action != Action.NO_ACTION,
159
+ ),
160
+ )
161
+ used[i] = true
162
+ }
163
+ else -> Unit
164
+ }
165
+ }
166
+
167
+ return TempivoSensorConfiguration(alerts, decompileSchedule(cfg.ruleCalendars))
168
+ }
169
+
170
+ private fun orSelectedPairs(rules: List<Rule>): List<Pair<Int, Int>> {
171
+ val out = mutableListOf<Pair<Int, Int>>()
172
+ for (r in rules) {
173
+ val c = r.condition as? LogicOperatorCondition ?: continue
174
+ val p = c.conditionParameters
175
+ if (p.logicOperator != LogicOperator.OR) continue
176
+ val ids = p.selectedRules.sorted()
177
+ if (ids.size == 2) out.add(ids[0] to ids[1])
178
+ }
179
+ return out
180
+ }
181
+
182
+ private fun channelOf(rule: Rule): String? =
183
+ when {
184
+ rule.channels.contains(3) -> "probe"
185
+ rule.channels.contains(1) -> "ambient"
186
+ else -> null
187
+ }
188
+
189
+ private fun findPair(used: BooleanArray): Int? {
190
+ for (i in 0..10) {
191
+ if (!used[i] && !used[i + 1]) return i
192
+ }
193
+ return null
194
+ }
195
+
196
+ private fun findFree(used: BooleanArray): Int? {
197
+ for (i in 0 until 12) if (!used[i]) return i
198
+ return null
199
+ }
200
+
201
+ private fun putThreshold(
202
+ rules: MutableList<Rule>,
203
+ slot: Int,
204
+ channel: Int,
205
+ low: Boolean,
206
+ threshold: Double,
207
+ hyst: Double,
208
+ action: Action,
209
+ ) {
210
+ if (slot !in 0..11) return
211
+ val params =
212
+ TemperatureLowHighThresholdConditionParameters(
213
+ threshold,
214
+ hyst,
215
+ TriggeringMode.CONSECUTIVE_SAMPLES,
216
+ 1,
217
+ )
218
+ val condition = if (low) LowThresholdCondition(params) else HighThresholdCondition(params)
219
+ rules[slot] = Rule(slot + 1, setOf(channel), condition, action)
220
+ }
221
+
222
+ private fun decompileSchedule(calendars: List<RuleCalendar>?): TempivoSchedule {
223
+ val weekly = calendars.orEmpty().map { it.type }.filterIsInstance<RuleCalendar.Type.Weekly>().firstOrNull()
224
+ ?: return TempivoSchedule(always = true)
225
+ val days = weekly.days.map { dayToPartnerIndex(it) }.sorted()
226
+ val always = days.size == 7 && weekly.fromTime == 0 && weekly.toTime >= 1439
227
+ return if (always) {
228
+ TempivoSchedule(always = true)
229
+ } else {
230
+ TempivoSchedule(
231
+ always = false,
232
+ weekdays = days,
233
+ from = TempivoSensorProfile.hhmmFromMinutes(weekly.fromTime),
234
+ to = TempivoSensorProfile.hhmmFromMinutes(weekly.toTime),
235
+ utcOffsetMinutes = weekly.timezoneOffset,
236
+ )
237
+ }
238
+ }
239
+
240
+ private fun weeklyType(schedule: TempivoSchedule): RuleCalendar.Type {
241
+ if (schedule.always) {
242
+ return RuleCalendar.Type.Weekly(
243
+ days = RuleCalendar.Type.Weekly.Day.entries.toSet(),
244
+ fromTime = 0,
245
+ toTime = 1439,
246
+ timezoneOffset = 0,
247
+ )
248
+ }
249
+ val days = schedule.weekdays.map { partnerIndexToDay(it) }.toSet()
250
+ return RuleCalendar.Type.Weekly(
251
+ days = days,
252
+ fromTime = TempivoSensorProfile.minutesFromHhmm(schedule.from ?: "00:00"),
253
+ toTime = TempivoSensorProfile.minutesFromHhmm(schedule.to ?: "23:59"),
254
+ timezoneOffset = schedule.utcOffsetMinutes,
255
+ )
256
+ }
257
+
258
+ private fun partnerIndexToDay(index: Int): RuleCalendar.Type.Weekly.Day =
259
+ when (index) {
260
+ 0 -> RuleCalendar.Type.Weekly.Day.MONDAY
261
+ 1 -> RuleCalendar.Type.Weekly.Day.TUESDAY
262
+ 2 -> RuleCalendar.Type.Weekly.Day.WEDNESDAY
263
+ 3 -> RuleCalendar.Type.Weekly.Day.THURSDAY
264
+ 4 -> RuleCalendar.Type.Weekly.Day.FRIDAY
265
+ 5 -> RuleCalendar.Type.Weekly.Day.SATURDAY
266
+ else -> RuleCalendar.Type.Weekly.Day.SUNDAY
267
+ }
268
+
269
+ private fun dayToPartnerIndex(day: RuleCalendar.Type.Weekly.Day): Int =
270
+ when (day) {
271
+ RuleCalendar.Type.Weekly.Day.MONDAY -> 0
272
+ RuleCalendar.Type.Weekly.Day.TUESDAY -> 1
273
+ RuleCalendar.Type.Weekly.Day.WEDNESDAY -> 2
274
+ RuleCalendar.Type.Weekly.Day.THURSDAY -> 3
275
+ RuleCalendar.Type.Weekly.Day.FRIDAY -> 4
276
+ RuleCalendar.Type.Weekly.Day.SATURDAY -> 5
277
+ RuleCalendar.Type.Weekly.Day.SUNDAY -> 6
278
+ }
279
+ }
@@ -0,0 +1,168 @@
1
+ package com.tempivo.sensor.beacon
2
+
3
+ import android.app.Application
4
+ import kotlinx.coroutines.delay
5
+ import pl.efento.mobile.bluetooth.EfentoBluetooth
6
+ import pl.efento.mobile.bluetooth.api.SensorConnection
7
+ import pl.efento.mobile.bluetooth.api.SensorLegacyConnection
8
+ import pl.efento.mobile.bluetooth.exception.InvalidResetCodeException
9
+ import pl.efento.mobile.bluetooth.exception.UnsupportedCommandException
10
+ import pl.efento.mobile.bluetooth.model.BluetoothMacAddress
11
+ import pl.efento.mobile.bluetooth.model.sensor.Rule
12
+ import pl.efento.mobile.bluetooth.model.sensor.RuleCalendar
13
+ import pl.efento.mobile.bluetooth.model.sensor.SensorConfiguration
14
+
15
+ /**
16
+ * Internal adapter to the sensor BLE runtime. Keep vendor types inside this file.
17
+ */
18
+ internal object SensorBleRuntime {
19
+ private val initMonitor = Any()
20
+ @Volatile private var initialized = false
21
+
22
+ fun initialize(application: Application) {
23
+ if (initialized) return
24
+ synchronized(initMonitor) {
25
+ if (initialized) return
26
+ EfentoBluetooth.init(application)
27
+ initialized = true
28
+ }
29
+ }
30
+
31
+ fun mapError(e: Throwable): TempivoSensorException {
32
+ if (e is TempivoSensorException) return e
33
+ val className = e.javaClass.name
34
+ return when {
35
+ e is InvalidResetCodeException || className.contains("InvalidResetCode") ->
36
+ TempivoSensorException(TempivoSensorException.Code.INVALID_PIN, "Invalid PIN.", e)
37
+ e is UnsupportedCommandException || className.contains("UnsupportedCommand") ->
38
+ TempivoSensorException(TempivoSensorException.Code.UNSUPPORTED_COMMAND, "Unsupported command.", e)
39
+ className.contains("NotConnected") ->
40
+ TempivoSensorException(TempivoSensorException.Code.NOT_CONNECTED, "Not connected.", e)
41
+ else ->
42
+ TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Sensor request failed.", e)
43
+ }
44
+ }
45
+
46
+ suspend fun connect(
47
+ application: Application,
48
+ serial: String,
49
+ bluetoothMac: String,
50
+ pin: Int,
51
+ legacy: Boolean,
52
+ ): Session {
53
+ initialize(application)
54
+ val mac = BluetoothMacAddress(bluetoothMac)
55
+ // PIN stays on the connection: set, trigger, calibration writes, and many other commands check it.
56
+ return if (legacy) {
57
+ val c =
58
+ EfentoBluetooth.sensorLegacyConnection(
59
+ deviceID = serial,
60
+ bluetoothMacAddress = mac,
61
+ resetCode = pin,
62
+ encryptionKey = "",
63
+ )
64
+ c.connect()
65
+ if (pin >= 0) c.resetCode = pin
66
+ Session.Legacy(c)
67
+ } else {
68
+ val c =
69
+ EfentoBluetooth.sensorConnection(
70
+ deviceID = serial,
71
+ bluetoothMacAddress = mac,
72
+ resetCode = pin,
73
+ encryptionKey = "",
74
+ )
75
+ c.connect()
76
+ if (pin >= 0) c.resetCode = pin
77
+ Session.Modern(c)
78
+ }
79
+ }
80
+
81
+ suspend fun disconnect(session: Session?) {
82
+ when (session) {
83
+ is Session.Modern -> runCatching { session.connection.disconnect() }
84
+ is Session.Legacy -> runCatching { session.connection.disconnect() }
85
+ null -> Unit
86
+ }
87
+ }
88
+
89
+ suspend fun readConfiguration(session: Session): SensorConfiguration {
90
+ return when (session) {
91
+ is Session.Modern -> modernGetConfiguration(session.connection)
92
+ is Session.Legacy ->
93
+ throw TempivoSensorException(
94
+ TempivoSensorException.Code.UNSUPPORTED_COMMAND,
95
+ "Configuration rules require a current-generation sensor session.",
96
+ )
97
+ }
98
+ }
99
+
100
+ suspend fun writeRules(session: Session, rules: List<Rule>, calendars: List<RuleCalendar>) {
101
+ when (session) {
102
+ is Session.Modern -> {
103
+ val base = modernGetConfiguration(session.connection)
104
+ val next = base.copy(rules = rules.take(12), ruleCalendars = calendars.take(6))
105
+ session.connection.commands.setConfiguration(next) { }
106
+ }
107
+ is Session.Legacy ->
108
+ throw TempivoSensorException(
109
+ TempivoSensorException.Code.UNSUPPORTED_COMMAND,
110
+ "Configuration rules require a current-generation sensor session.",
111
+ )
112
+ }
113
+ }
114
+
115
+ suspend fun triggerTransmission(session: Session): TempivoSensorSession.TriggerResult {
116
+ return when (session) {
117
+ is Session.Modern -> {
118
+ session.connection.commands.triggerCommunicationWithServer { }
119
+ TempivoSensorSession.TriggerResult(ok = true, supported = true)
120
+ }
121
+ is Session.Legacy -> {
122
+ try {
123
+ session.connection.cellularCommands.triggerCommunicationWithServer { }
124
+ TempivoSensorSession.TriggerResult(ok = true, supported = true)
125
+ } catch (e: UnsupportedCommandException) {
126
+ TempivoSensorSession.TriggerResult(ok = false, supported = false)
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ suspend fun readCalibration(session: Session): TempivoSensorCalibration {
133
+ return when (session) {
134
+ is Session.Modern -> {
135
+ val ext = session.connection.commands.getExtendedConfiguration { }
136
+ TempivoSensorCalibrationDecoder.decode(ext.laboratoryCalibrationTimestamp)
137
+ }
138
+ is Session.Legacy -> TempivoSensorCalibration(null, null)
139
+ }
140
+ }
141
+
142
+ fun compileRules(cfg: TempivoSensorConfiguration): Pair<List<Rule>, List<RuleCalendar>> =
143
+ PartnerBleRules.compile(cfg)
144
+
145
+ fun decompile(cfg: SensorConfiguration): TempivoSensorConfiguration =
146
+ PartnerBleRules.decompile(cfg)
147
+
148
+ private suspend fun modernGetConfiguration(modern: SensorConnection): SensorConfiguration {
149
+ var last: Exception? = null
150
+ repeat(3) { attempt ->
151
+ try {
152
+ if (attempt > 0) {
153
+ runCatching { modern.commands.getDeviceInfo { } }
154
+ }
155
+ return modern.commands.getConfiguration { }
156
+ } catch (e: Exception) {
157
+ last = e
158
+ if (attempt < 2) delay(400)
159
+ }
160
+ }
161
+ throw last ?: TempivoSensorException(TempivoSensorException.Code.UNKNOWN, "Could not read configuration.")
162
+ }
163
+
164
+ sealed class Session {
165
+ class Modern(val connection: SensorConnection) : Session()
166
+ class Legacy(val connection: SensorLegacyConnection) : Session()
167
+ }
168
+ }