@tempivo/sensor-beacon 1.1.1 → 1.1.4
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 +43 -0
- package/README.md +38 -23
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +8 -0
- package/android/src/main/java/expo/modules/tempivosensorbeacon/TempivoSensorBeaconModule.kt +8 -0
- package/android-aar/library/build.gradle +5 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/EfentoRawConfigTransfer.kt +672 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +188 -52
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +20 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +28 -0
- package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +142 -11
- package/android-aar/library/src/test/java/com/tempivo/sensor/beacon/PartnerBleRulesTest.kt +209 -0
- package/dist/native-helpers.js +3 -1
- package/dist/native-module.d.ts +10 -0
- package/dist/native-module.js +46 -3
- package/dist/partner-dual-range-fixture.d.ts +28 -0
- package/dist/partner-dual-range-fixture.js +29 -0
- package/dist/profile.js +66 -32
- package/dist/proto/decode-proto-config.d.ts +3 -0
- package/dist/proto/decode-proto-config.js +46 -0
- package/dist/proto/efento-protobuf-descriptor.d.ts +964 -0
- package/dist/proto/efento-protobuf-descriptor.js +727 -0
- package/dist/proto/partner-from-proto.d.ts +9 -0
- package/dist/proto/partner-from-proto.js +187 -0
- package/dist/proto/partner-to-proto.d.ts +12 -0
- package/dist/proto/partner-to-proto.js +211 -0
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
- package/ios/Sources/TempivoSensorBeacon/EfentoRawConfigTransfer.swift +601 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +99 -29
- package/ios/TempivoSensorBeaconModule.swift +9 -0
- package/package.json +6 -2
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
package com.tempivo.sensor.beacon
|
|
2
|
+
|
|
3
|
+
import org.junit.Assert.assertEquals
|
|
4
|
+
import org.junit.Assert.assertFalse
|
|
5
|
+
import org.junit.Assert.assertTrue
|
|
6
|
+
import org.junit.Test
|
|
7
|
+
import pl.efento.mobile.bluetooth.model.sensor.Action
|
|
8
|
+
import pl.efento.mobile.bluetooth.model.sensor.DisabledCondition
|
|
9
|
+
import pl.efento.mobile.bluetooth.model.sensor.HighThresholdCondition
|
|
10
|
+
import pl.efento.mobile.bluetooth.model.sensor.LogicOperator
|
|
11
|
+
import pl.efento.mobile.bluetooth.model.sensor.LogicOperatorCondition
|
|
12
|
+
import pl.efento.mobile.bluetooth.model.sensor.LowThresholdCondition
|
|
13
|
+
import pl.efento.mobile.bluetooth.model.sensor.Rule
|
|
14
|
+
|
|
15
|
+
class PartnerBleRulesTest {
|
|
16
|
+
private val simon =
|
|
17
|
+
TempivoSensorConfiguration(
|
|
18
|
+
temperatureAlerts =
|
|
19
|
+
listOf(
|
|
20
|
+
TempivoTemperatureAlert(
|
|
21
|
+
type = "range",
|
|
22
|
+
channel = "ambient",
|
|
23
|
+
lowC = 2.0,
|
|
24
|
+
highC = 8.0,
|
|
25
|
+
hysteresisC = 0.5,
|
|
26
|
+
transmitOnBreach = true,
|
|
27
|
+
transmitOnReturn = true,
|
|
28
|
+
),
|
|
29
|
+
TempivoTemperatureAlert(
|
|
30
|
+
type = "range",
|
|
31
|
+
channel = "probe",
|
|
32
|
+
lowC = -30.0,
|
|
33
|
+
highC = -18.0,
|
|
34
|
+
hysteresisC = 0.5,
|
|
35
|
+
transmitOnBreach = true,
|
|
36
|
+
transmitOnReturn = true,
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
schedule = TempivoSchedule(always = true),
|
|
40
|
+
measurementIntervalMinutes = 5.0,
|
|
41
|
+
transmissionIntervalSeconds = 3600,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun compileDecompileSimonJsonKeepsBothRanges() {
|
|
46
|
+
val (rules, _) = PartnerBleRules.compile(simon)
|
|
47
|
+
val kinds = rules.take(6).map { kind(it) }
|
|
48
|
+
assertEquals(listOf("LOW", "HIGH", "OR", "LOW", "HIGH", "OR"), kinds)
|
|
49
|
+
|
|
50
|
+
val got = PartnerBleRules.decompile(rules)
|
|
51
|
+
assertEquals(2, got.temperatureAlerts.size)
|
|
52
|
+
assertEquals("ambient", got.temperatureAlerts[0].channel)
|
|
53
|
+
assertEquals(2.0, got.temperatureAlerts[0].lowC!!, 0.01)
|
|
54
|
+
assertEquals(8.0, got.temperatureAlerts[0].highC!!, 0.01)
|
|
55
|
+
assertEquals("probe", got.temperatureAlerts[1].channel)
|
|
56
|
+
assertEquals(-30.0, got.temperatureAlerts[1].lowC!!, 0.01)
|
|
57
|
+
assertEquals(-18.0, got.temperatureAlerts[1].highC!!, 0.01)
|
|
58
|
+
assertTrue(got.temperatureAlerts.all { it.transmitOnReturn })
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test
|
|
62
|
+
fun setSameJsonTwiceIsAccepted() {
|
|
63
|
+
val (rules, _) = PartnerBleRules.compile(simon)
|
|
64
|
+
val empty = List(12) { i -> Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION) }
|
|
65
|
+
|
|
66
|
+
val first = PartnerBleRules.configurationWriteAccepted(simon.temperatureAlerts, rules, empty, rules)
|
|
67
|
+
assertTrue(first)
|
|
68
|
+
|
|
69
|
+
val second = PartnerBleRules.configurationWriteAccepted(simon.temperatureAlerts, rules, rules, rules)
|
|
70
|
+
assertTrue(second)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@Test
|
|
74
|
+
fun oldReadbackCountCheckFailsWhenDecompileIsEmpty() {
|
|
75
|
+
val (rules, _) = PartnerBleRules.compile(simon)
|
|
76
|
+
val emptyGet = PartnerBleRules.decompile(emptyList()).temperatureAlerts
|
|
77
|
+
assertEquals(0, emptyGet.size)
|
|
78
|
+
assertTrue(emptyGet.size < simon.temperatureAlerts.size)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test
|
|
82
|
+
fun liveDeskProtoDecompilesAndIdempotentSetIsAccepted() {
|
|
83
|
+
// Live get from desk sensor after Simon JSON (channel_mask 1 / 4, 0.1 °C units).
|
|
84
|
+
val fromDevice =
|
|
85
|
+
listOf(
|
|
86
|
+
threshold(1, 1, true, 2.0, 0.5, Action.NO_ACTION),
|
|
87
|
+
threshold(2, 1, false, 8.0, 0.5, Action.NO_ACTION),
|
|
88
|
+
orSlot(3, setOf(1, 2)),
|
|
89
|
+
threshold(4, 3, true, -30.0, 0.5, Action.NO_ACTION),
|
|
90
|
+
threshold(5, 3, false, -18.0, 0.5, Action.NO_ACTION),
|
|
91
|
+
orSlot(6, setOf(4, 5)),
|
|
92
|
+
) + List(6) { i -> Rule(i + 7, emptySet(), DisabledCondition, Action.NO_ACTION) }
|
|
93
|
+
|
|
94
|
+
val got = PartnerBleRules.decompile(fromDevice)
|
|
95
|
+
assertEquals(2, got.temperatureAlerts.size)
|
|
96
|
+
assertEquals(listOf("ambient", "probe"), got.temperatureAlerts.map { it.channel })
|
|
97
|
+
|
|
98
|
+
val (intended, _) = PartnerBleRules.compile(simon)
|
|
99
|
+
assertTrue(PartnerBleRules.compiledRulesMatchDevice(intended, fromDevice))
|
|
100
|
+
assertTrue(
|
|
101
|
+
PartnerBleRules.configurationWriteAccepted(
|
|
102
|
+
simon.temperatureAlerts,
|
|
103
|
+
intended,
|
|
104
|
+
fromDevice,
|
|
105
|
+
fromDevice,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@Test
|
|
111
|
+
fun zeroBasedSlotsDecompileToAmbientAndProbe() {
|
|
112
|
+
val fromDevice =
|
|
113
|
+
listOf(
|
|
114
|
+
threshold(1, 0, true, 2.0, 0.5, Action.NO_ACTION),
|
|
115
|
+
threshold(2, 0, false, 8.0, 0.5, Action.NO_ACTION),
|
|
116
|
+
orSlot(3, setOf(1, 2)),
|
|
117
|
+
threshold(4, 2, true, -30.0, 0.5, Action.NO_ACTION),
|
|
118
|
+
threshold(5, 2, false, -18.0, 0.5, Action.NO_ACTION),
|
|
119
|
+
orSlot(6, setOf(4, 5)),
|
|
120
|
+
) + List(6) { i -> Rule(i + 7, emptySet(), DisabledCondition, Action.NO_ACTION) }
|
|
121
|
+
val got = PartnerBleRules.decompile(fromDevice)
|
|
122
|
+
assertEquals(2, got.temperatureAlerts.size)
|
|
123
|
+
assertEquals(listOf("ambient", "probe"), got.temperatureAlerts.map { it.channel })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
@Test
|
|
127
|
+
fun fw7ExtraRuleSlotsStillDecompileById() {
|
|
128
|
+
val active =
|
|
129
|
+
listOf(
|
|
130
|
+
threshold(1, 1, true, 2.0, 0.5, Action.NO_ACTION),
|
|
131
|
+
threshold(2, 1, false, 8.0, 0.5, Action.NO_ACTION),
|
|
132
|
+
orSlot(3, setOf(1, 2)),
|
|
133
|
+
threshold(4, 3, true, -30.0, 0.5, Action.NO_ACTION),
|
|
134
|
+
threshold(5, 3, false, -18.0, 0.5, Action.NO_ACTION),
|
|
135
|
+
orSlot(6, setOf(4, 5)),
|
|
136
|
+
)
|
|
137
|
+
// Simulate FW7 returning >12 entries: padding first, then real ids.
|
|
138
|
+
val padding = List(12) { i -> Rule(100 + i, emptySet(), DisabledCondition, Action.NO_ACTION) }
|
|
139
|
+
val fromDevice = padding + active
|
|
140
|
+
val got = PartnerBleRules.decompile(fromDevice)
|
|
141
|
+
assertEquals(2, got.temperatureAlerts.size)
|
|
142
|
+
assertEquals(listOf("ambient", "probe"), got.temperatureAlerts.map { it.channel })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@Test
|
|
146
|
+
fun emptyGetAfterSameConfigIsStillAccepted() {
|
|
147
|
+
val (rules, _) = PartnerBleRules.compile(simon)
|
|
148
|
+
val empty = List(12) { i -> Rule(i + 1, emptySet(), DisabledCondition, Action.NO_ACTION) }
|
|
149
|
+
assertTrue(
|
|
150
|
+
PartnerBleRules.configurationWriteAccepted(simon.temperatureAlerts, rules, rules, empty),
|
|
151
|
+
)
|
|
152
|
+
// Blind HC7 get (empty before and after) must not reject a successful write.
|
|
153
|
+
assertTrue(
|
|
154
|
+
PartnerBleRules.configurationWriteAccepted(simon.temperatureAlerts, rules, empty, empty),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun threshold(
|
|
159
|
+
id: Int,
|
|
160
|
+
channel: Int,
|
|
161
|
+
low: Boolean,
|
|
162
|
+
t: Double,
|
|
163
|
+
hyst: Double,
|
|
164
|
+
action: Action,
|
|
165
|
+
): Rule {
|
|
166
|
+
val params =
|
|
167
|
+
pl.efento.mobile.bluetooth.model.sensor.TemperatureLowHighThresholdConditionParameters(
|
|
168
|
+
t,
|
|
169
|
+
hyst,
|
|
170
|
+
pl.efento.mobile.bluetooth.model.sensor.TriggeringMode.CONSECUTIVE_SAMPLES,
|
|
171
|
+
1,
|
|
172
|
+
)
|
|
173
|
+
val condition =
|
|
174
|
+
if (low) {
|
|
175
|
+
pl.efento.mobile.bluetooth.model.sensor.LowThresholdCondition(params)
|
|
176
|
+
} else {
|
|
177
|
+
pl.efento.mobile.bluetooth.model.sensor.HighThresholdCondition(params)
|
|
178
|
+
}
|
|
179
|
+
return Rule(id, setOf(channel), condition, action)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private fun orSlot(id: Int, selected: Set<Int>): Rule =
|
|
183
|
+
Rule(
|
|
184
|
+
id,
|
|
185
|
+
emptySet(),
|
|
186
|
+
pl.efento.mobile.bluetooth.model.sensor.LogicOperatorCondition(
|
|
187
|
+
pl.efento.mobile.bluetooth.model.sensor.LogicOperatorConditionParameters(
|
|
188
|
+
LogicOperator.OR,
|
|
189
|
+
selected,
|
|
190
|
+
emptySet(),
|
|
191
|
+
0,
|
|
192
|
+
0,
|
|
193
|
+
),
|
|
194
|
+
),
|
|
195
|
+
Action.TRIGGER_TRANSMISSION_WITH_ACK,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
private fun kind(rule: Rule): String =
|
|
199
|
+
when (rule.condition) {
|
|
200
|
+
is LowThresholdCondition -> "LOW"
|
|
201
|
+
is HighThresholdCondition -> "HIGH"
|
|
202
|
+
is LogicOperatorCondition -> {
|
|
203
|
+
val p = (rule.condition as LogicOperatorCondition).conditionParameters
|
|
204
|
+
if (p.logicOperator == LogicOperator.OR) "OR" else "LOGIC"
|
|
205
|
+
}
|
|
206
|
+
is DisabledCondition -> "OFF"
|
|
207
|
+
else -> rule.condition.javaClass.simpleName
|
|
208
|
+
}
|
|
209
|
+
}
|
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/native-module.d.ts
CHANGED
|
@@ -8,7 +8,17 @@ export declare function isScanning(): boolean;
|
|
|
8
8
|
export declare function addDeviceFoundListener(listener: (device: TempivoSensorBeaconDevice) => void): TempivoDeviceFoundSubscription;
|
|
9
9
|
export declare function connect(qr: string | TempivoSensorQr): Promise<TempivoNativeQr>;
|
|
10
10
|
export declare function disconnect(): Promise<void>;
|
|
11
|
+
/**
|
|
12
|
+
* Partner configuration from the device.
|
|
13
|
+
* Return shape is only {@link TempivoSensorConfiguration} (no debug fields).
|
|
14
|
+
* iOS and Android use raw ProtoConfig GATT; return shape is only partner fields.
|
|
15
|
+
*/
|
|
11
16
|
export declare function getConfiguration(): Promise<TempivoSensorConfiguration>;
|
|
17
|
+
/**
|
|
18
|
+
* Write partner configuration to the device.
|
|
19
|
+
* Same public signature as before. Encodes partner config to ProtoConfig and writes raw GATT
|
|
20
|
+
* when `writeProtoConfigBase64` is available (iOS and Android).
|
|
21
|
+
*/
|
|
12
22
|
export declare function setConfigurationJson(config: string | TempivoSensorConfiguration): Promise<void>;
|
|
13
23
|
export declare function triggerTransmission(): Promise<TempivoTriggerTransmissionResult>;
|
|
14
24
|
export declare function getCalibration(): Promise<TempivoNativeCalibration>;
|
package/dist/native-module.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { requireOptionalNativeModule } from 'expo-modules-core';
|
|
2
2
|
import { TempivoSensorError } from './errors.js';
|
|
3
3
|
import { configToJson, mapNativeError, qrToConnectJson } from './native-helpers.js';
|
|
4
|
-
import {
|
|
4
|
+
import { partnerConfigFromProtoBase64 } from './proto/partner-from-proto.js';
|
|
5
|
+
import { partnerConfigToProtoBase64 } from './proto/partner-to-proto.js';
|
|
6
|
+
import { parseSensorConfigurationFromDevice } from './profile.js';
|
|
5
7
|
function getNative() {
|
|
6
8
|
const native = requireOptionalNativeModule('TempivoSensorBeacon');
|
|
7
9
|
if (!native) {
|
|
@@ -67,24 +69,65 @@ export async function connect(qr) {
|
|
|
67
69
|
}
|
|
68
70
|
export async function disconnect() {
|
|
69
71
|
try {
|
|
72
|
+
lastProtoBase64 = null;
|
|
70
73
|
await getNative().disconnect();
|
|
71
74
|
}
|
|
72
75
|
catch (error) {
|
|
73
76
|
throw mapNativeError(error);
|
|
74
77
|
}
|
|
75
78
|
}
|
|
79
|
+
/** Last ProtoConfig from iOS raw get; used as set baseline. Not part of the partner API. */
|
|
80
|
+
let lastProtoBase64 = null;
|
|
81
|
+
/**
|
|
82
|
+
* Partner configuration from the device.
|
|
83
|
+
* Return shape is only {@link TempivoSensorConfiguration} (no debug fields).
|
|
84
|
+
* iOS and Android use raw ProtoConfig GATT; return shape is only partner fields.
|
|
85
|
+
*/
|
|
76
86
|
export async function getConfiguration() {
|
|
77
87
|
try {
|
|
78
88
|
const json = await getNative().getConfigurationJson();
|
|
79
|
-
|
|
89
|
+
const parsed = JSON.parse(json);
|
|
90
|
+
const protoB64 = typeof parsed.protoBase64 === 'string' ? parsed.protoBase64 : null;
|
|
91
|
+
if (protoB64) {
|
|
92
|
+
lastProtoBase64 = protoB64;
|
|
93
|
+
return partnerConfigFromProtoBase64(protoB64);
|
|
94
|
+
}
|
|
95
|
+
lastProtoBase64 = null;
|
|
96
|
+
return parseSensorConfigurationFromDevice(parsed);
|
|
80
97
|
}
|
|
81
98
|
catch (error) {
|
|
82
99
|
throw mapNativeError(error);
|
|
83
100
|
}
|
|
84
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Write partner configuration to the device.
|
|
104
|
+
* Same public signature as before. Encodes partner config to ProtoConfig and writes raw GATT
|
|
105
|
+
* when `writeProtoConfigBase64` is available (iOS and Android).
|
|
106
|
+
*/
|
|
85
107
|
export async function setConfigurationJson(config) {
|
|
86
108
|
try {
|
|
87
|
-
|
|
109
|
+
const partner = typeof config === 'string'
|
|
110
|
+
? JSON.parse(config)
|
|
111
|
+
: config;
|
|
112
|
+
const native = getNative();
|
|
113
|
+
if (typeof native.writeProtoConfigBase64 === 'function') {
|
|
114
|
+
let baseline = lastProtoBase64;
|
|
115
|
+
if (!baseline) {
|
|
116
|
+
await getConfiguration();
|
|
117
|
+
baseline = lastProtoBase64;
|
|
118
|
+
}
|
|
119
|
+
if (!baseline) {
|
|
120
|
+
throw new TempivoSensorError('invalidConfig', 'Cannot set: missing baseline ProtoConfig from device get.');
|
|
121
|
+
}
|
|
122
|
+
const nextB64 = partnerConfigToProtoBase64(partner, baseline);
|
|
123
|
+
await native.writeProtoConfigBase64(nextB64);
|
|
124
|
+
lastProtoBase64 = nextB64;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (typeof native.setConfigurationJson !== 'function') {
|
|
128
|
+
throw new TempivoSensorError('runtimeUnavailable', 'Native setConfigurationJson is unavailable in this build.');
|
|
129
|
+
}
|
|
130
|
+
await native.setConfigurationJson(configToJson(partner));
|
|
88
131
|
}
|
|
89
132
|
catch (error) {
|
|
90
133
|
throw mapNativeError(error);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partner BLE dual-range + always payload used in regression tests.
|
|
3
|
+
* Before SDK 1.1.2, get could return empty alerts and/or Sunday-only schedule.
|
|
4
|
+
*/
|
|
5
|
+
export declare const PARTNER_DUAL_RANGE_ALWAYS_BODY: {
|
|
6
|
+
schedule: {
|
|
7
|
+
always: boolean;
|
|
8
|
+
};
|
|
9
|
+
temperatureAlerts: ({
|
|
10
|
+
type: "range";
|
|
11
|
+
channel: "ambient";
|
|
12
|
+
lowC: number;
|
|
13
|
+
highC: number;
|
|
14
|
+
hysteresisC: number;
|
|
15
|
+
transmitOnBreach: boolean;
|
|
16
|
+
transmitOnReturn: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
type: "range";
|
|
19
|
+
channel: "probe";
|
|
20
|
+
lowC: number;
|
|
21
|
+
highC: number;
|
|
22
|
+
hysteresisC: number;
|
|
23
|
+
transmitOnBreach: boolean;
|
|
24
|
+
transmitOnReturn: boolean;
|
|
25
|
+
})[];
|
|
26
|
+
measurementIntervalMinutes: number;
|
|
27
|
+
transmissionIntervalSeconds: number;
|
|
28
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partner BLE dual-range + always payload used in regression tests.
|
|
3
|
+
* Before SDK 1.1.2, get could return empty alerts and/or Sunday-only schedule.
|
|
4
|
+
*/
|
|
5
|
+
export const PARTNER_DUAL_RANGE_ALWAYS_BODY = {
|
|
6
|
+
schedule: { always: true },
|
|
7
|
+
temperatureAlerts: [
|
|
8
|
+
{
|
|
9
|
+
type: 'range',
|
|
10
|
+
channel: 'ambient',
|
|
11
|
+
lowC: 2,
|
|
12
|
+
highC: 8,
|
|
13
|
+
hysteresisC: 0.5,
|
|
14
|
+
transmitOnBreach: true,
|
|
15
|
+
transmitOnReturn: true,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
type: 'range',
|
|
19
|
+
channel: 'probe',
|
|
20
|
+
lowC: -30,
|
|
21
|
+
highC: -18,
|
|
22
|
+
hysteresisC: 0.5,
|
|
23
|
+
transmitOnBreach: true,
|
|
24
|
+
transmitOnReturn: true,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
measurementIntervalMinutes: 5,
|
|
28
|
+
transmissionIntervalSeconds: 3600,
|
|
29
|
+
};
|
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)) {
|
|
@@ -198,35 +218,49 @@ export const BLE_MAX_TRANSMISSION_INTERVAL_SECONDS = 604_800;
|
|
|
198
218
|
function parseOptionalIntervals(body, options = {}) {
|
|
199
219
|
const validatePartnerLimits = !options.fromDevice;
|
|
200
220
|
const out = {};
|
|
201
|
-
if (body.measurementIntervalMinutes !== undefined) {
|
|
221
|
+
if (body.measurementIntervalMinutes !== undefined && body.measurementIntervalMinutes !== null) {
|
|
202
222
|
const n = parseFiniteNumber(body.measurementIntervalMinutes);
|
|
203
223
|
if (n === undefined || !Number.isFinite(n) || n <= 0) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
throw new TempivoSensorError('invalidConfig',
|
|
224
|
+
if (options.fromDevice) {
|
|
225
|
+
// Vendor may emit 0/null; omit rather than fail the whole get.
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
throw new TempivoSensorError('invalidConfig', 'measurementIntervalMinutes must be a positive number.');
|
|
209
229
|
}
|
|
210
|
-
|
|
211
|
-
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
if (validatePartnerLimits) {
|
|
233
|
+
if (n < BLE_MIN_MEASUREMENT_INTERVAL_MINUTES) {
|
|
234
|
+
throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at least ${BLE_MIN_MEASUREMENT_INTERVAL_MINUTES}.`);
|
|
235
|
+
}
|
|
236
|
+
if (n > BLE_MAX_MEASUREMENT_INTERVAL_MINUTES) {
|
|
237
|
+
throw new TempivoSensorError('invalidConfig', `measurementIntervalMinutes must be at most ${BLE_MAX_MEASUREMENT_INTERVAL_MINUTES}.`);
|
|
238
|
+
}
|
|
212
239
|
}
|
|
240
|
+
out.measurementIntervalMinutes = n;
|
|
213
241
|
}
|
|
214
|
-
out.measurementIntervalMinutes = n;
|
|
215
242
|
}
|
|
216
|
-
if (body.transmissionIntervalSeconds !== undefined) {
|
|
243
|
+
if (body.transmissionIntervalSeconds !== undefined && body.transmissionIntervalSeconds !== null) {
|
|
217
244
|
const n = parseFiniteNumber(body.transmissionIntervalSeconds);
|
|
218
|
-
if (n === undefined || !Number.
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
throw new TempivoSensorError('invalidConfig',
|
|
245
|
+
if (n === undefined || !Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
|
|
246
|
+
if (options.fromDevice) {
|
|
247
|
+
// omit
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
throw new TempivoSensorError('invalidConfig', 'transmissionIntervalSeconds must be a positive integer.');
|
|
224
251
|
}
|
|
225
|
-
|
|
226
|
-
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
if (validatePartnerLimits) {
|
|
255
|
+
if (n < BLE_MIN_TRANSMISSION_INTERVAL_SECONDS) {
|
|
256
|
+
throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at least ${BLE_MIN_TRANSMISSION_INTERVAL_SECONDS} (1 hour).`);
|
|
257
|
+
}
|
|
258
|
+
if (n > BLE_MAX_TRANSMISSION_INTERVAL_SECONDS) {
|
|
259
|
+
throw new TempivoSensorError('invalidConfig', `transmissionIntervalSeconds must be at most ${BLE_MAX_TRANSMISSION_INTERVAL_SECONDS}.`);
|
|
260
|
+
}
|
|
227
261
|
}
|
|
262
|
+
out.transmissionIntervalSeconds = n;
|
|
228
263
|
}
|
|
229
|
-
out.transmissionIntervalSeconds = n;
|
|
230
264
|
}
|
|
231
265
|
if (validatePartnerLimits &&
|
|
232
266
|
out.measurementIntervalMinutes != null &&
|
|
@@ -249,7 +283,7 @@ function parseSensorConfigurationBody(body, options = {}) {
|
|
|
249
283
|
const intervals = parseOptionalIntervals(body, options);
|
|
250
284
|
return {
|
|
251
285
|
temperatureAlerts,
|
|
252
|
-
schedule: parseSchedule(body.schedule),
|
|
286
|
+
schedule: parseSchedule(body.schedule, options.fromDevice === true),
|
|
253
287
|
...intervals,
|
|
254
288
|
};
|
|
255
289
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decode Efento ProtoConfig bytes (same descriptor as web bluetooth module).
|
|
3
|
+
*/
|
|
4
|
+
import protobuf from 'protobufjs/light.js';
|
|
5
|
+
import descriptor from './efento-protobuf-descriptor.js';
|
|
6
|
+
const root = protobuf.Root.fromJSON(descriptor);
|
|
7
|
+
const ProtoConfig = root.lookupType('ProtoConfig');
|
|
8
|
+
export function decodeProtoConfigBytes(bytes) {
|
|
9
|
+
const msg = ProtoConfig.decode(bytes);
|
|
10
|
+
return ProtoConfig.toObject(msg, {
|
|
11
|
+
enums: String,
|
|
12
|
+
longs: Number,
|
|
13
|
+
bytes: Array,
|
|
14
|
+
arrays: true,
|
|
15
|
+
objects: true,
|
|
16
|
+
oneofs: true,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function base64ToBytes(b64) {
|
|
20
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
21
|
+
const clean = b64.replace(/=+$/, '').replace(/[^A-Za-z0-9+/]/g, '');
|
|
22
|
+
const out = new Uint8Array((clean.length * 3) >> 2);
|
|
23
|
+
let o = 0;
|
|
24
|
+
for (let i = 0; i < clean.length; i += 4) {
|
|
25
|
+
const a = chars.indexOf(clean[i] ?? 'A');
|
|
26
|
+
const b = chars.indexOf(clean[i + 1] ?? 'A');
|
|
27
|
+
const c = chars.indexOf(clean[i + 2] ?? 'A');
|
|
28
|
+
const d = chars.indexOf(clean[i + 3] ?? 'A');
|
|
29
|
+
out[o++] = (a << 2) | (b >> 4);
|
|
30
|
+
if (clean[i + 2] != null)
|
|
31
|
+
out[o++] = ((b & 15) << 4) | (c >> 2);
|
|
32
|
+
if (clean[i + 3] != null)
|
|
33
|
+
out[o++] = ((c & 3) << 6) | d;
|
|
34
|
+
}
|
|
35
|
+
return out.subarray(0, o);
|
|
36
|
+
}
|
|
37
|
+
export function decodeProtoConfigBase64(b64) {
|
|
38
|
+
if (typeof atob === 'function') {
|
|
39
|
+
const binary = atob(b64);
|
|
40
|
+
const bytes = new Uint8Array(binary.length);
|
|
41
|
+
for (let i = 0; i < binary.length; i++)
|
|
42
|
+
bytes[i] = binary.charCodeAt(i);
|
|
43
|
+
return decodeProtoConfigBytes(bytes);
|
|
44
|
+
}
|
|
45
|
+
return decodeProtoConfigBytes(base64ToBytes(b64));
|
|
46
|
+
}
|