@tempivo/sensor-beacon 0.1.1 → 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.
- package/README.md +195 -79
- package/android/library/build.gradle +3 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/PartnerBleRules.kt +279 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +168 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +6 -23
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +0 -2
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -4
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorCalibration.kt +36 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorError.kt +18 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorProfile.kt +171 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +82 -0
- package/android/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +114 -0
- package/android/settings.gradle +1 -0
- package/dist/calibration.d.ts +7 -0
- package/dist/calibration.js +42 -0
- package/dist/decoder.js +4 -12
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +8 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -1
- package/dist/profile.d.ts +10 -0
- package/dist/profile.js +226 -0
- package/dist/qr.d.ts +16 -0
- package/dist/qr.js +90 -0
- package/dist/session-types.d.ts +64 -0
- package/dist/session-types.js +1 -0
- package/dist/tempivo-sensor-beacon.aar +0 -0
- package/dist/types.d.ts +3 -3
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +4 -8
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconTypes.swift +3 -6
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorCalibration.swift +22 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorError.swift +24 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +123 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +78 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +161 -0
- package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +93 -0
- package/package.json +10 -3
package/README.md
CHANGED
|
@@ -1,129 +1,245 @@
|
|
|
1
1
|
# @tempivo/sensor-beacon
|
|
2
2
|
|
|
3
|
-
Tempivo partner SDK for
|
|
3
|
+
Tempivo partner SDK for sensors over BLE. Advertisement decode (manufacturer id **0x026C**) plus GATT session for alert rules, an immediate uplink, and laboratory calibration date.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
```bash
|
|
6
|
+
npm install @tempivo/sensor-beacon
|
|
7
|
+
```
|
|
6
8
|
|
|
7
9
|
| Path | Use |
|
|
8
10
|
|------|-----|
|
|
9
|
-
| `dist/` | Node / TypeScript
|
|
10
|
-
| `android/` + `dist/tempivo-sensor-beacon.aar` | Android scan +
|
|
11
|
-
| `ios/` | iOS Swift Package (scan +
|
|
11
|
+
| `dist/` | Node / TypeScript: advertising decode, QR parse, config JSON |
|
|
12
|
+
| `android/` + `dist/tempivo-sensor-beacon.aar` | Android scan + GATT |
|
|
13
|
+
| `ios/` | iOS Swift Package (scan + session) |
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
npm install @tempivo/sensor-beacon
|
|
15
|
-
```
|
|
15
|
+
GATT runs on a phone (Android / iOS), not in Node. Always pass the sticker PIN to `connect`. Wrong PIN maps to `invalidPin`.
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## QR object
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Sticker QR is JSON. Parse with `parseSensorQrJson` (Node) or `TempivoSensorQrParser.parse` (Android / iOS).
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{ "sn": "282C024F0012", "pin": "111111", "model": "HC7" }
|
|
23
|
+
```
|
|
20
24
|
|
|
21
25
|
```ts
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
normalizeManufacturerBytes,
|
|
25
|
-
decodeSensorBeaconPayload,
|
|
26
|
-
} from '@tempivo/sensor-beacon';
|
|
26
|
+
const qr = parseSensorQrJson(stickerQrText);
|
|
27
|
+
```
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
| Field | Type | Meaning |
|
|
30
|
+
|-------|------|---------|
|
|
31
|
+
| `serial` | string | Device serial, 12 hex chars, no colons. From QR `sn`. |
|
|
32
|
+
| `pin` | string | Sticker PIN. Required on `connect`. Also accepted as QR `resetCode`. |
|
|
33
|
+
| `model` | string | Sticker model. Omitted QR `model` becomes **HC7**. `HC5` selects legacy GATT. |
|
|
34
|
+
| `sessionType` | `modern` \| `legacy` | GATT path. HC7 / firmware 7+ is `modern`. HC5 / firmware 6.x is `legacy`. |
|
|
35
|
+
| `bluetoothMac` | string | BLE MAC with colons, derived from `serial`. |
|
|
36
|
+
|
|
37
|
+
## Config JSON (same as Cellular API)
|
|
38
|
+
|
|
39
|
+
`setConfiguration` / `getConfiguration` use the same `temperatureAlerts` + `schedule` shape as `POST /api/v1/devices/config-profiles`.
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"schedule": { "always": true },
|
|
44
|
+
"temperatureAlerts": [
|
|
45
|
+
{
|
|
46
|
+
"type": "range",
|
|
47
|
+
"channel": "probe",
|
|
48
|
+
"lowC": -30,
|
|
49
|
+
"highC": -10,
|
|
50
|
+
"hysteresisC": 0.5,
|
|
51
|
+
"transmitOnBreach": true,
|
|
52
|
+
"transmitOnReturn": true
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
29
56
|
```
|
|
30
57
|
|
|
31
|
-
|
|
58
|
+
**Root**
|
|
59
|
+
|
|
60
|
+
| Field | Meaning |
|
|
61
|
+
|-------|---------|
|
|
62
|
+
| `temperatureAlerts` | Alert list. Empty array is valid (no temperature alerts). |
|
|
63
|
+
| `schedule` | When alerts are active. `{ "always": true }` is 24/7. |
|
|
64
|
+
|
|
65
|
+
Ignored over BLE: `slug`, `name`, `measurementIntervalMinutes`, `transmissionIntervalSeconds`. Do not pass `GET /devices/{serial}/config` (`alarmRules`).
|
|
66
|
+
|
|
67
|
+
**Each `temperatureAlerts[]` item**
|
|
68
|
+
|
|
69
|
+
| Field | Meaning |
|
|
70
|
+
|-------|---------|
|
|
71
|
+
| `type` | `range` (band), `min` (below), or `max` (above). |
|
|
72
|
+
| `channel` | `ambient` or `probe`. |
|
|
73
|
+
| `lowC` / `highC` | Inclusive band for `type: "range"`. |
|
|
74
|
+
| `minC` | Threshold for `type: "min"`. |
|
|
75
|
+
| `maxC` | Threshold for `type: "max"`. |
|
|
76
|
+
| `hysteresisC` | Degrees of hysteresis. Omitted: **1**. |
|
|
77
|
+
| `transmitOnBreach` | Uplink when the alert trips. Omitted: **true**. |
|
|
78
|
+
| `transmitOnReturn` | Extra uplink when temp returns inside a `range`. Omitted: **true**. Range only. |
|
|
32
79
|
|
|
33
|
-
|
|
80
|
+
**`schedule` weekday window** (instead of `always`)
|
|
34
81
|
|
|
35
|
-
|
|
82
|
+
| Field | Meaning |
|
|
83
|
+
|-------|---------|
|
|
84
|
+
| `weekdays` | **0 = Monday** … **6 = Sunday**. |
|
|
85
|
+
| `from` / `to` | 24h `HH:MM`. |
|
|
86
|
+
| `utcOffsetMinutes` | Offset from UTC for `from` / `to`. |
|
|
87
|
+
|
|
88
|
+
## Fetch a profile, apply over BLE
|
|
89
|
+
|
|
90
|
+
`GET /api/v1/devices/config-profiles/{slug}` returns `{ "profile": { … } }`. Pass the GET body or `profile` to `setConfigurationJson`.
|
|
36
91
|
|
|
37
92
|
```ts
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
93
|
+
import { parseSensorQrJson, configurationFromApiProfile } from '@tempivo/sensor-beacon';
|
|
94
|
+
|
|
95
|
+
const qr = parseSensorQrJson(stickerQrText);
|
|
96
|
+
const res = await fetch(
|
|
97
|
+
`https://app.tempivo.com/api/v1/devices/config-profiles/${slug}`,
|
|
98
|
+
{ headers: { Authorization: `Bearer ${apiKey}` } }
|
|
99
|
+
);
|
|
100
|
+
const body = await res.json();
|
|
101
|
+
const cfg = configurationFromApiProfile(body); // or body.profile
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
On the phone, after `connect(qr)`:
|
|
44
105
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
ingestManufacturerPayload(scanPayload, macKey, cache);
|
|
49
|
-
const reading = buildSensorBeaconReading(cache.last03.get(macKey), cache.last04.get(macKey));
|
|
106
|
+
```kotlin
|
|
107
|
+
session.setConfigurationJson(profileJson) // GET body or profile object
|
|
108
|
+
session.triggerTransmission()
|
|
50
109
|
```
|
|
51
110
|
|
|
52
|
-
|
|
111
|
+
BLE does not write measurement interval, transmission interval, server, APN, name, or token.
|
|
53
112
|
|
|
54
|
-
|
|
55
|
-
|--------|---------|
|
|
56
|
-
| `TEMPVO_SENSOR_MANUFACTURER_ID` | BLE company id `0x026c` |
|
|
57
|
-
| `normalizeManufacturerBytes` | Strip company id prefix when present |
|
|
58
|
-
| `decodeSensorBeaconPayload` | One-shot decode |
|
|
59
|
-
| `ingestManufacturerPayload` | Update frame cache |
|
|
60
|
-
| `buildSensorBeaconReading` | Merge cached frames |
|
|
61
|
-
| `SensorBeaconReading` | Parsed device + measurements |
|
|
113
|
+
## Node / TypeScript helpers
|
|
62
114
|
|
|
63
|
-
|
|
115
|
+
```ts
|
|
116
|
+
import {
|
|
117
|
+
normalizeManufacturerBytes,
|
|
118
|
+
decodeSensorBeaconPayload,
|
|
119
|
+
parseSensorQrJson,
|
|
120
|
+
parseSensorConfigurationJson,
|
|
121
|
+
} from '@tempivo/sensor-beacon';
|
|
64
122
|
|
|
65
|
-
|
|
123
|
+
const qr = parseSensorQrJson('{"sn":"282C024F0012","pin":"111111","model":"HC7"}');
|
|
124
|
+
const reading = decodeSensorBeaconPayload(normalizeManufacturerBytes(advertisementBytes));
|
|
125
|
+
const cfg = parseSensorConfigurationJson(/* config JSON above */);
|
|
126
|
+
```
|
|
66
127
|
|
|
67
|
-
|
|
128
|
+
Invalid QR or config throws `TempivoSensorError` (`invalidQr`, `invalidPin`, `invalidConfig`).
|
|
129
|
+
|
|
130
|
+
## Android (GATT)
|
|
131
|
+
|
|
132
|
+
`connect` / `setConfigurationJson` / `triggerTransmission` are `suspend`. Request `BLUETOOTH_SCAN` and `BLUETOOTH_CONNECT` (API 31+) or location on older APIs.
|
|
68
133
|
|
|
69
134
|
```kotlin
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
)
|
|
135
|
+
val qr = TempivoSensorQrParser.parse(stickerQrText)
|
|
136
|
+
val session = TempivoSensorSession(context)
|
|
137
|
+
|
|
138
|
+
session.connect(qr)
|
|
139
|
+
try {
|
|
140
|
+
session.setConfigurationJson(profileJson)
|
|
141
|
+
val trigger = session.triggerTransmission()
|
|
142
|
+
val applied = session.getConfiguration()
|
|
143
|
+
val cal = session.getCalibration()
|
|
144
|
+
} finally {
|
|
145
|
+
session.disconnect()
|
|
77
146
|
}
|
|
78
147
|
```
|
|
79
148
|
|
|
80
|
-
|
|
149
|
+
**`triggerTransmission()`**
|
|
81
150
|
|
|
82
|
-
|
|
83
|
-
|
|
151
|
+
| Field | Meaning |
|
|
152
|
+
|-------|---------|
|
|
153
|
+
| `ok` | Device accepted the uplink request. |
|
|
154
|
+
| `supported` | Firmware implements the command. `false` is not an error. |
|
|
84
155
|
|
|
85
|
-
|
|
86
|
-
scanner.startScan { device ->
|
|
87
|
-
// device.serialNumber, device.rssi, device.telemetry, device.summary
|
|
88
|
-
}
|
|
89
|
-
```
|
|
156
|
+
**`getCalibration()`**
|
|
90
157
|
|
|
91
|
-
|
|
158
|
+
| Field | Meaning |
|
|
159
|
+
|-------|---------|
|
|
160
|
+
| `laboratoryCalibrationDate` | ISO date (`YYYY-MM-DD`), or null if unset. |
|
|
161
|
+
| `laboratoryCalibrationTimestamp` | Same instant as unix seconds, or null. No expiry field on the device. |
|
|
92
162
|
|
|
93
|
-
## iOS
|
|
163
|
+
## iOS (GATT)
|
|
94
164
|
|
|
95
|
-
|
|
96
|
-
2. Path: `node_modules/@tempivo/sensor-beacon/ios`
|
|
97
|
-
3. Add product **TempivoSensorBeacon**
|
|
98
|
-
4. Set **`NSBluetoothAlwaysUsageDescription`** in Info.plist
|
|
165
|
+
Needs a physical device and the native `TempivoSensorBridge` XCFramework (`scripts/build-ios-bridge.sh`). Set **`NSBluetoothAlwaysUsageDescription`** in Info.plist. Simulator can scan advertisements only.
|
|
99
166
|
|
|
100
167
|
```swift
|
|
101
|
-
|
|
168
|
+
let qr = try TempivoSensorQrParser.parse(stickerQrText)
|
|
169
|
+
let session = TempivoSensorSession()
|
|
170
|
+
try session.connect(qr: qr)
|
|
171
|
+
defer { session.disconnect() }
|
|
172
|
+
try session.setConfigurationJson(profileJson)
|
|
173
|
+
_ = try session.triggerTransmission()
|
|
174
|
+
let applied = try session.getConfiguration()
|
|
175
|
+
let cal = try session.getCalibration()
|
|
176
|
+
```
|
|
102
177
|
|
|
103
|
-
|
|
104
|
-
private let scanner = TempivoSensorBeaconScanner()
|
|
178
|
+
## Advertising (no pairing)
|
|
105
179
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
180
|
+
Filter manufacturer data on company id **0x026C**. Use **active scanning** so scan-response packets arrive. Product model is **not** in the broadcast. Connect uses QR `model` (HC7 is modern GATT).
|
|
181
|
+
|
|
182
|
+
```kotlin
|
|
183
|
+
val scanner = TempivoSensorBeaconScanner(context)
|
|
184
|
+
scanner.startScan { device ->
|
|
185
|
+
Log.d("scan", "${device.serialNumber} rssi=${device.rssi} ${device.summary}")
|
|
186
|
+
}
|
|
187
|
+
```
|
|
110
188
|
|
|
111
|
-
|
|
189
|
+
**Android `TempivoSensorBeaconDevice`**
|
|
190
|
+
|
|
191
|
+
| Field | Meaning |
|
|
192
|
+
|-------|---------|
|
|
193
|
+
| `deviceId` | Android BLE address. Often randomized. Not the sticker serial. |
|
|
194
|
+
| `bluetoothMacAddress` | MAC from the advertisement, with colons. |
|
|
195
|
+
| `serialNumber` | 12 hex chars from advertisement frame `0x03`. Same as QR `sn`. |
|
|
196
|
+
| `rssi` | Signal strength in dBm. |
|
|
197
|
+
| `summary` | Short display line. After scan response: measurement texts joined with ` · `. Before that: serial, or `Adv OK. Wait for scan response (measurements).` |
|
|
198
|
+
| `telemetry` | Decoded frames `0x03` + `0x04`, or null until enough bytes arrive. |
|
|
199
|
+
|
|
200
|
+
**`telemetry` (Android) / `reading` (iOS and Node)**
|
|
201
|
+
|
|
202
|
+
| Field | Meaning |
|
|
203
|
+
|-------|---------|
|
|
204
|
+
| `firmware` | `major.minor.patch` from the advertisement, e.g. `7.3.4`. |
|
|
205
|
+
| `batteryOk` | Battery status flag from the advertisement. |
|
|
206
|
+
| `encryptionEnabled` | Advertisement payload encryption flag. |
|
|
207
|
+
| `cellularStatus` | `ble_only`, `cell_ok`, `no_server`, or `net_issue`. |
|
|
208
|
+
| `readingTimestampUnix` | Sample time as unix seconds, or null. |
|
|
209
|
+
| `readingTimestampIso` | Same instant as UTC ISO-8601, or null. |
|
|
210
|
+
| `measurementIntervalSeconds` | Sample interval from the advertisement, in seconds. |
|
|
211
|
+
| `readings` | Decoded scan-response slots (see below). |
|
|
212
|
+
| `readingsCount` | Number of measurement slots. |
|
|
213
|
+
| `summary` | Same short line as on the device object. |
|
|
214
|
+
| `measurementCounter` | Counter from frame `0x03`, or null. |
|
|
215
|
+
|
|
216
|
+
Node also exposes `serialMac`, `temperatures[]`, `humidity`, and `rawHex`.
|
|
217
|
+
|
|
218
|
+
**Each `readings[]` / `measurements[]` slot**
|
|
219
|
+
|
|
220
|
+
| Field | Meaning |
|
|
221
|
+
|-------|---------|
|
|
222
|
+
| `typeHex` | Slot type, e.g. `0x01` temperature, `0x02` humidity. |
|
|
223
|
+
| `raw24` | Undecoded 24-bit payload. |
|
|
224
|
+
| `text` | Display value with unit, e.g. `21.8 °C`. No type name in the string. |
|
|
225
|
+
| `temperatureC` / `humidityPct` | Node only: numeric value when the slot is that type. |
|
|
226
|
+
|
|
227
|
+
iOS `SensorBeaconDiscovery` has `deviceId`, `rssi`, `serialNumber`, `bluetoothMacAddress`, and `reading` (same telemetry fields).
|
|
112
228
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
229
|
+
```swift
|
|
230
|
+
func sensorBeaconScanner(_ scanner: TempivoSensorBeaconScanner, didDiscover discovery: SensorBeaconDiscovery) {
|
|
231
|
+
print(discovery.serialNumber ?? "", discovery.rssi, discovery.reading?.summary ?? "")
|
|
116
232
|
}
|
|
117
233
|
```
|
|
118
234
|
|
|
119
|
-
|
|
235
|
+
## Errors
|
|
236
|
+
|
|
237
|
+
Native GATT maps to: `invalidPin`, `invalidQr`, `invalidConfig`, `notConnected`, `unsupportedCommand`, `connectFailed`, `runtimeUnavailable`, `unknown`.
|
|
120
238
|
|
|
121
|
-
##
|
|
239
|
+
## Tests
|
|
122
240
|
|
|
123
241
|
```bash
|
|
124
|
-
npm run
|
|
125
|
-
npm run build:android # requires Android SDK
|
|
126
|
-
npm run build:all # both
|
|
242
|
+
npm run test:unit
|
|
127
243
|
```
|
|
128
244
|
|
|
129
245
|
## License
|
|
@@ -24,4 +24,7 @@ android {
|
|
|
24
24
|
|
|
25
25
|
dependencies {
|
|
26
26
|
implementation "androidx.core:core-ktx:1.17.0"
|
|
27
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
|
|
28
|
+
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2"
|
|
29
|
+
implementation "pl.efento.mobile:bluetooth:2.3.0"
|
|
27
30
|
}
|
|
@@ -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
|
+
}
|