@tempivo/sensor-beacon 0.4.3 → 1.1.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 (45) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +156 -6
  3. package/android/src/main/java/expo/modules/tempivosensorbeacon/SensorBeaconNative.java +17 -2
  4. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/BleConnectCredentials.kt +25 -0
  5. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SdkMeasurementFormat.kt +111 -0
  6. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleRuntime.kt +21 -12
  7. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorBleVendor.kt +28 -0
  8. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorModelHint.kt +20 -0
  9. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/SensorSdkScanTelemetry.kt +5 -31
  10. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconDecoder.kt +1 -1
  11. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconScanner.kt +5 -6
  12. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorBeaconTypes.kt +2 -0
  13. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorQr.kt +20 -1
  14. package/android-aar/library/src/main/java/com/tempivo/sensor/beacon/TempivoSensorSession.kt +19 -7
  15. package/dist/connect-helpers.d.ts +52 -0
  16. package/dist/connect-helpers.js +153 -0
  17. package/dist/decoder.d.ts +1 -0
  18. package/dist/decoder.js +7 -1
  19. package/dist/index.d.ts +6 -2
  20. package/dist/index.js +4 -2
  21. package/dist/model.d.ts +5 -0
  22. package/dist/model.js +25 -0
  23. package/dist/native-helpers.js +5 -2
  24. package/dist/native-module.js +2 -2
  25. package/dist/native-types.d.ts +2 -0
  26. package/dist/profile.d.ts +8 -1
  27. package/dist/profile.js +42 -19
  28. package/dist/react-native.d.ts +5 -1
  29. package/dist/react-native.js +3 -1
  30. package/dist/session-types.d.ts +2 -1
  31. package/dist/tempivo-sensor-beacon.aar +0 -0
  32. package/ios/Frameworks/TempivoSensorBridge.xcframework/Info.plist +4 -4
  33. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  34. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  35. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/Headers/TempivoSensorBridge.h +28 -1
  36. package/ios/Frameworks/TempivoSensorBridge.xcframework/ios-arm64-simulator/TempivoSensorBridge.framework/TempivoSensorBridge +0 -0
  37. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconDecoder.swift +8 -1
  38. package/ios/Sources/TempivoSensorBeacon/TempivoSensorBeaconScanner.swift +10 -1
  39. package/ios/Sources/TempivoSensorBeacon/TempivoSensorModelHint.swift +29 -0
  40. package/ios/Sources/TempivoSensorBeacon/TempivoSensorProfile.swift +37 -13
  41. package/ios/Sources/TempivoSensorBeacon/TempivoSensorQr.swift +38 -1
  42. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSession.swift +24 -12
  43. package/ios/Sources/TempivoSensorBeacon/TempivoSensorSessionTypes.swift +1 -3
  44. package/ios/TempivoSensorBeaconModule.swift +83 -16
  45. package/package.json +4 -3
@@ -5,8 +5,12 @@ export { MEASURE_SPECS, type MeasureSpec } from './measure-specs.js';
5
5
  export { createSensorBeaconFrameCache, type SensorBeaconFrameCache, type SensorBeaconMeasurement, type SensorBeaconReading, } from './types.js';
6
6
  export { TempivoSensorError, type TempivoSensorErrorCode } from './errors.js';
7
7
  export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
+ export { isMissingAdvertisementFirmware, sensorModelFromFirmware } from './model.js';
9
+ export type { SensorStickerModel } from './model.js';
10
+ export { buildSensorQrConnectJson, draftSensorQrConnectJsonFromScanDevice, formatSensorReadingLine, mergeScanTargetIntoSensorQr, normalizeSensorStickerModel, parseSensorQrForConnect, pinFromSensorQrRecord, resolveSensorBleConnectCredentialsFromPin, sensorQrConnectJsonFromLoose, sensorQrSerialMatchesSelected, serialHexFromSensorQrScan, tryParseSensorQrJson, } from './connect-helpers.js';
11
+ export type { ParsedSensorQrLoose, SensorBleConnectCredentials } from './connect-helpers.js';
8
12
  export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
9
- export { BLE_MAX_MEASUREMENT_INTERVAL_MINUTES, BLE_MAX_TRANSMISSION_INTERVAL_SECONDS, BLE_MIN_MEASUREMENT_INTERVAL_MINUTES, BLE_MIN_TRANSMISSION_INTERVAL_SECONDS, DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
13
+ export { BLE_MAX_MEASUREMENT_INTERVAL_MINUTES, BLE_MAX_TRANSMISSION_INTERVAL_SECONDS, BLE_MIN_MEASUREMENT_INTERVAL_MINUTES, BLE_MIN_TRANSMISSION_INTERVAL_SECONDS, DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationFromDevice, parseSensorConfigurationFromDeviceJson, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
14
  export type { TempivoSchedule, TempivoScheduleAlways, TempivoScheduleWeek, TempivoSensorCalibration, TempivoSensorConfiguration, TempivoSensorQr, TempivoSensorSessionType, TempivoTemperatureAlert, TempivoTemperatureChannel, TempivoTemperatureMaxAlert, TempivoTemperatureMinAlert, TempivoTemperatureRangeAlert, TempivoTriggerTransmissionResult, } from './session-types.js';
11
15
  export type { TempivoDeviceFoundSubscription, TempivoNativeCalibration, TempivoNativeMeasurement, TempivoNativePermissionResponse, TempivoNativePermissionStatus, TempivoNativeQr, TempivoNativeTelemetry, TempivoSensorBeaconDevice, } from './native-types.js';
12
16
  import * as native from './native-module.js';
@@ -5,8 +5,10 @@ export { MEASURE_SPECS } from './measure-specs.js';
5
5
  export { createSensorBeaconFrameCache, } from './types.js';
6
6
  export { TempivoSensorError } from './errors.js';
7
7
  export { DEFAULT_SENSOR_MODEL, bluetoothMacFromSerial, normalizeSensorSerial, encodeSensorPinPayload, parseSensorQrJson, sessionTypeFromModel, } from './qr.js';
8
+ export { isMissingAdvertisementFirmware, sensorModelFromFirmware } from './model.js';
9
+ export { buildSensorQrConnectJson, draftSensorQrConnectJsonFromScanDevice, formatSensorReadingLine, mergeScanTargetIntoSensorQr, normalizeSensorStickerModel, parseSensorQrForConnect, pinFromSensorQrRecord, resolveSensorBleConnectCredentialsFromPin, sensorQrConnectJsonFromLoose, sensorQrSerialMatchesSelected, serialHexFromSensorQrScan, tryParseSensorQrJson, } from './connect-helpers.js';
8
10
  export { calibrationFromExtendedJson, decodeLaboratoryCalibrationTimestamp, } from './calibration.js';
9
- export { BLE_MAX_MEASUREMENT_INTERVAL_MINUTES, BLE_MAX_TRANSMISSION_INTERVAL_SECONDS, BLE_MIN_MEASUREMENT_INTERVAL_MINUTES, BLE_MIN_TRANSMISSION_INTERVAL_SECONDS, DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
11
+ export { BLE_MAX_MEASUREMENT_INTERVAL_MINUTES, BLE_MAX_TRANSMISSION_INTERVAL_SECONDS, BLE_MIN_MEASUREMENT_INTERVAL_MINUTES, BLE_MIN_TRANSMISSION_INTERVAL_SECONDS, DEFAULT_ALERT_HYSTERESIS_C, compileSensorProfileToBleJson, configurationFromApiProfile, parseSensorConfiguration, parseSensorConfigurationFromDevice, parseSensorConfigurationFromDeviceJson, parseSensorConfigurationJson, pickPartnerConfigurationJson, } from './profile.js';
10
12
  import * as native from './native-module.js';
11
13
  export const isNativeSensorBeaconAvailable = native.isNativeSensorBeaconAvailable;
12
14
  export const requestPermissions = native.requestPermissions;
@@ -60,6 +60,8 @@ export type TempivoSensorQr = {
60
60
  /** Always `modern` (HC7 GATT). */
61
61
  sessionType: TempivoSensorSessionType;
62
62
  bluetoothMac: string;
63
+ /** SDK scan `deviceId` when it differs from sticker serial. */
64
+ deviceId?: string;
63
65
  };
64
66
  export type TempivoSensorCalibration = {
65
67
  laboratoryCalibrationDate: string | null;
@@ -67,5 +69,4 @@ export type TempivoSensorCalibration = {
67
69
  };
68
70
  export type TempivoTriggerTransmissionResult = {
69
71
  ok: boolean;
70
- supported: boolean;
71
72
  };
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</string>
11
+ <string>ios-arm64-simulator</string>
12
12
  <key>LibraryPath</key>
13
13
  <string>TempivoSensorBridge.framework</string>
14
14
  <key>SupportedArchitectures</key>
@@ -17,12 +17,14 @@
17
17
  </array>
18
18
  <key>SupportedPlatform</key>
19
19
  <string>ios</string>
20
+ <key>SupportedPlatformVariant</key>
21
+ <string>simulator</string>
20
22
  </dict>
21
23
  <dict>
22
24
  <key>BinaryPath</key>
23
25
  <string>TempivoSensorBridge.framework/TempivoSensorBridge</string>
24
26
  <key>LibraryIdentifier</key>
25
- <string>ios-arm64-simulator</string>
27
+ <string>ios-arm64</string>
26
28
  <key>LibraryPath</key>
27
29
  <string>TempivoSensorBridge.framework</string>
28
30
  <key>SupportedArchitectures</key>
@@ -31,8 +33,6 @@
31
33
  </array>
32
34
  <key>SupportedPlatform</key>
33
35
  <string>ios</string>
34
- <key>SupportedPlatformVariant</key>
35
- <string>simulator</string>
36
36
  </dict>
37
37
  </array>
38
38
  <key>CFBundlePackageType</key>
@@ -6,6 +6,8 @@
6
6
  #import <Foundation/NSString.h>
7
7
  #import <Foundation/NSValue.h>
8
8
 
9
+ @class TSBSensorDeviceScanHandler;
10
+
9
11
  NS_ASSUME_NONNULL_BEGIN
10
12
  #pragma clang diagnostic push
11
13
  #pragma clang diagnostic ignored "-Wunknown-warning-option"
@@ -140,17 +142,42 @@ __attribute__((swift_name("KotlinBoolean")))
140
142
  + (instancetype)numberWithBool:(BOOL)value;
141
143
  @end
142
144
 
145
+
146
+ /**
147
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
148
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
149
+ */
150
+ __attribute__((swift_name("SensorDeviceScanHandler")))
151
+ @interface TSBSensorDeviceScanHandler : TSBBase
152
+
153
+ /**
154
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
155
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
156
+ */
157
+ - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
158
+
159
+ /**
160
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
161
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
162
+ */
163
+ + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
164
+ - (void)onDeviceDeviceJson:(NSString *)deviceJson __attribute__((swift_name("onDevice(deviceJson:)")));
165
+ @end
166
+
143
167
  __attribute__((objc_subclassing_restricted))
144
168
  __attribute__((swift_name("TempivoSensorBridge")))
145
169
  @interface TSBTempivoSensorBridge : TSBBase
146
170
  - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
147
171
  + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
148
- - (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:)")));
172
+ - (NSString *)connectPayloadJsonDeviceId:(NSString *)deviceId bluetoothMac:(NSString *)bluetoothMac pin:(NSString *)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(deviceId:bluetoothMac:pin:legacy:)")));
149
173
  - (void)disconnect __attribute__((swift_name("disconnect()")));
150
174
  - (NSString *)getCalibrationPayloadJson __attribute__((swift_name("getCalibrationPayloadJson()")));
151
175
  - (NSString *)getConfigurationPayloadJson __attribute__((swift_name("getConfigurationPayloadJson()")));
152
176
  - (void)initialize __attribute__((swift_name("initialize()")));
153
177
  - (NSString *)setConfigurationPayloadJsonJson:(NSString *)json __attribute__((swift_name("setConfigurationPayloadJson(json:)")));
178
+ - (void)setDeviceScanHandlerHandler:(TSBSensorDeviceScanHandler * _Nullable)handler __attribute__((swift_name("setDeviceScanHandler(handler:)")));
179
+ - (void)startDeviceScan __attribute__((swift_name("startDeviceScan()")));
180
+ - (void)stopDeviceScan __attribute__((swift_name("stopDeviceScan()")));
154
181
  - (NSString *)triggerTransmissionPayloadJson __attribute__((swift_name("triggerTransmissionPayloadJson()")));
155
182
  @end
156
183
 
@@ -6,6 +6,8 @@
6
6
  #import <Foundation/NSString.h>
7
7
  #import <Foundation/NSValue.h>
8
8
 
9
+ @class TSBSensorDeviceScanHandler;
10
+
9
11
  NS_ASSUME_NONNULL_BEGIN
10
12
  #pragma clang diagnostic push
11
13
  #pragma clang diagnostic ignored "-Wunknown-warning-option"
@@ -140,17 +142,42 @@ __attribute__((swift_name("KotlinBoolean")))
140
142
  + (instancetype)numberWithBool:(BOOL)value;
141
143
  @end
142
144
 
145
+
146
+ /**
147
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
148
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
149
+ */
150
+ __attribute__((swift_name("SensorDeviceScanHandler")))
151
+ @interface TSBSensorDeviceScanHandler : TSBBase
152
+
153
+ /**
154
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
155
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
156
+ */
157
+ - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
158
+
159
+ /**
160
+ * Swift receives [TempivoSensorBridge.startDeviceScan] events here.
161
+ * JSON matches the inner device object from Android scan (MAC, serial, advertisementTelemetry).
162
+ */
163
+ + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
164
+ - (void)onDeviceDeviceJson:(NSString *)deviceJson __attribute__((swift_name("onDevice(deviceJson:)")));
165
+ @end
166
+
143
167
  __attribute__((objc_subclassing_restricted))
144
168
  __attribute__((swift_name("TempivoSensorBridge")))
145
169
  @interface TSBTempivoSensorBridge : TSBBase
146
170
  - (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
147
171
  + (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
148
- - (NSString *)connectPayloadJsonSerial:(NSString *)serial bluetoothMac:(NSString *)bluetoothMac pin:(int32_t)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(serial:bluetoothMac:pin:legacy:)")));
172
+ - (NSString *)connectPayloadJsonDeviceId:(NSString *)deviceId bluetoothMac:(NSString *)bluetoothMac pin:(NSString *)pin legacy:(BOOL)legacy __attribute__((swift_name("connectPayloadJson(deviceId:bluetoothMac:pin:legacy:)")));
149
173
  - (void)disconnect __attribute__((swift_name("disconnect()")));
150
174
  - (NSString *)getCalibrationPayloadJson __attribute__((swift_name("getCalibrationPayloadJson()")));
151
175
  - (NSString *)getConfigurationPayloadJson __attribute__((swift_name("getConfigurationPayloadJson()")));
152
176
  - (void)initialize __attribute__((swift_name("initialize()")));
153
177
  - (NSString *)setConfigurationPayloadJsonJson:(NSString *)json __attribute__((swift_name("setConfigurationPayloadJson(json:)")));
178
+ - (void)setDeviceScanHandlerHandler:(TSBSensorDeviceScanHandler * _Nullable)handler __attribute__((swift_name("setDeviceScanHandler(handler:)")));
179
+ - (void)startDeviceScan __attribute__((swift_name("startDeviceScan()")));
180
+ - (void)stopDeviceScan __attribute__((swift_name("stopDeviceScan()")));
154
181
  - (NSString *)triggerTransmissionPayloadJson __attribute__((swift_name("triggerTransmissionPayloadJson()")));
155
182
  @end
156
183
 
@@ -250,6 +250,13 @@ public enum TempivoSensorBeaconDecoder {
250
250
  return (n >> 1) ^ (-(n & 1))
251
251
  }
252
252
 
253
+ private static func measurementTypeLabel(_ mtype: UInt8) -> String {
254
+ guard let spec = measureTypes[mtype] else {
255
+ return "type_\(String(format: "%02x", mtype))"
256
+ }
257
+ return spec.name.lowercased()
258
+ }
259
+
253
260
  private static func decodeSlot(mtype: UInt8, raw24: UInt32) -> String {
254
261
  guard let spec = measureTypes[mtype] else {
255
262
  return String(format: "0x%02X: 0x%06X", mtype, raw24)
@@ -356,7 +363,7 @@ public enum TempivoSensorBeaconDecoder {
356
363
  summaryParts.append(text)
357
364
  readings.append(
358
365
  SensorBeaconMeasurement(
359
- typeHex: String(format: "0x%02X", mtype),
366
+ typeHex: measurementTypeLabel(mtype),
360
367
  raw24: Int(raw24 & 0xFFFFFF),
361
368
  text: text
362
369
  )
@@ -17,6 +17,7 @@ public final class TempivoSensorBeaconScanner: NSObject {
17
17
 
18
18
  private var central: CBCentralManager!
19
19
  private var frameCache = SensorBeaconFrameCache()
20
+ private var peripheralIdToSerialKey: [String: String] = [:]
20
21
  private var isScanning = false
21
22
  private var pendingStart = false
22
23
 
@@ -36,6 +37,7 @@ public final class TempivoSensorBeaconScanner: NSObject {
36
37
  /// Clears cached `0x03` / `0x04` frames. Does not stop an active scan.
37
38
  public func clearCache() {
38
39
  frameCache.clear()
40
+ peripheralIdToSerialKey.removeAll(keepingCapacity: false)
39
41
  }
40
42
 
41
43
  /// Starts an active scan (duplicate advertisements allowed). Requires Bluetooth powered on.
@@ -109,13 +111,20 @@ public final class TempivoSensorBeaconScanner: NSObject {
109
111
  if let mac = TempivoSensorBeaconDecoder.macFromManufacturerData(mdata) {
110
112
  bluetoothMacAddress = mac
111
113
  serialNumber = TempivoSensorBeaconDecoder.macKey(mac)
114
+ if let serialNumber {
115
+ peripheralIdToSerialKey[deviceId] = serialNumber.uppercased()
116
+ }
112
117
  }
113
- let primaryKey = serialNumber ?? deviceId
118
+ let serialHint = serialNumber ?? peripheralIdToSerialKey[deviceId]
119
+ let primaryKey = serialHint ?? deviceId
114
120
  _ = TempivoSensorBeaconDecoder.ingestManufacturerData(
115
121
  mdata,
116
122
  primaryMacKey: primaryKey,
117
123
  cache: &frameCache
118
124
  )
125
+ if serialNumber == nil {
126
+ serialNumber = serialHint
127
+ }
119
128
  }
120
129
 
121
130
  let keys = cacheKeys(serial: serialNumber, macWithColons: bluetoothMacAddress, deviceId: deviceId)
@@ -0,0 +1,29 @@
1
+ import Foundation
2
+
3
+ enum TempivoSensorModelHint {
4
+ static let defaultModel = "HC7"
5
+
6
+ static func isMissingFirmware(_ firmware: String?) -> Bool {
7
+ let t = firmware?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
8
+ return t.isEmpty || t == "0.0.0"
9
+ }
10
+
11
+ static func fromFirmware(_ firmware: String?) -> String {
12
+ if isMissingFirmware(firmware) { return defaultModel }
13
+ let t = firmware!.trimmingCharacters(in: .whitespacesAndNewlines)
14
+ if t.range(of: "^FW 5", options: [.regularExpression, .caseInsensitive]) != nil {
15
+ return "HC5"
16
+ }
17
+ let pattern = #"^(?:FW\s*)?(\d+)\.\d+"#
18
+ guard let regex = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive),
19
+ let match = regex.firstMatch(in: t, range: NSRange(t.startIndex..., in: t)),
20
+ match.numberOfRanges > 1,
21
+ let majorRange = Range(match.range(at: 1), in: t),
22
+ let major = Int(t[majorRange]) else {
23
+ return defaultModel
24
+ }
25
+ if major >= 7 { return "HC7" }
26
+ if major >= 1 { return "HC5" }
27
+ return defaultModel
28
+ }
29
+ }
@@ -10,7 +10,24 @@ public enum TempivoSensorProfile {
10
10
  return try parse(obj)
11
11
  }
12
12
 
13
+ public static func parseFromDevice(_ obj: [String: Any]) throws -> TempivoSensorConfiguration {
14
+ try parseBody(obj, validatePartnerLimits: false)
15
+ }
16
+
17
+ public static func parseFromDevice(json: String) throws -> TempivoSensorConfiguration {
18
+ guard let data = json.data(using: .utf8),
19
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
20
+ else {
21
+ throw TempivoSensorError(.invalidConfig, "Configuration is not valid JSON.")
22
+ }
23
+ return try parseFromDevice(obj)
24
+ }
25
+
13
26
  public static func parse(_ obj: [String: Any]) throws -> TempivoSensorConfiguration {
27
+ try parseBody(obj, validatePartnerLimits: true)
28
+ }
29
+
30
+ private static func parseBody(_ obj: [String: Any], validatePartnerLimits: Bool) throws -> TempivoSensorConfiguration {
14
31
  if obj["profiles"] is [Any] {
15
32
  throw TempivoSensorError(.invalidConfig, "Pass one profile from GET /devices/config-profiles/{slug}, not the list.")
16
33
  }
@@ -30,7 +47,7 @@ public enum TempivoSensorProfile {
30
47
  }
31
48
  let rawAlerts = body["temperatureAlerts"] as? [Any] ?? []
32
49
  let alerts = try rawAlerts.enumerated().map { try parseAlert($0.element, index: $0.offset) }
33
- let intervals = try parseOptionalIntervals(body)
50
+ let intervals = try parseOptionalIntervals(body, validatePartnerLimits: validatePartnerLimits)
34
51
  return TempivoSensorConfiguration(
35
52
  temperatureAlerts: alerts,
36
53
  schedule: try parseSchedule(body["schedule"] as? [String: Any]),
@@ -78,18 +95,23 @@ public enum TempivoSensorProfile {
78
95
  private static let minTransmissionSeconds = 3600
79
96
  private static let maxTransmissionSeconds = 604_800
80
97
 
81
- private static func parseOptionalIntervals(_ body: [String: Any]) throws -> (measurement: Double?, transmission: Int?) {
98
+ private static func parseOptionalIntervals(
99
+ _ body: [String: Any],
100
+ validatePartnerLimits: Bool
101
+ ) throws -> (measurement: Double?, transmission: Int?) {
82
102
  var measurement: Double?
83
103
  var transmission: Int?
84
104
  if body["measurementIntervalMinutes"] != nil {
85
105
  guard let n = number(body["measurementIntervalMinutes"]), n > 0 else {
86
106
  throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be a positive number.")
87
107
  }
88
- if n < minMeasurementMinutes {
89
- throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at least \(Int(minMeasurementMinutes)) (1 hour).")
90
- }
91
- if n > maxMeasurementMinutes {
92
- throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at most \(Int(maxMeasurementMinutes)).")
108
+ if validatePartnerLimits {
109
+ if n < minMeasurementMinutes {
110
+ throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at least \(Int(minMeasurementMinutes)) (1 hour).")
111
+ }
112
+ if n > maxMeasurementMinutes {
113
+ throw TempivoSensorError(.invalidConfig, "measurementIntervalMinutes must be at most \(Int(maxMeasurementMinutes)).")
114
+ }
93
115
  }
94
116
  measurement = n
95
117
  }
@@ -98,15 +120,17 @@ public enum TempivoSensorProfile {
98
120
  throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be a positive integer.")
99
121
  }
100
122
  let sec = Int(n)
101
- if sec < minTransmissionSeconds {
102
- throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at least \(minTransmissionSeconds) (1 hour).")
103
- }
104
- if sec > maxTransmissionSeconds {
105
- throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at most \(maxTransmissionSeconds).")
123
+ if validatePartnerLimits {
124
+ if sec < minTransmissionSeconds {
125
+ throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at least \(minTransmissionSeconds) (1 hour).")
126
+ }
127
+ if sec > maxTransmissionSeconds {
128
+ throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at most \(maxTransmissionSeconds).")
129
+ }
106
130
  }
107
131
  transmission = sec
108
132
  }
109
- if let measurement, let transmission {
133
+ if validatePartnerLimits, let measurement, let transmission {
110
134
  let maxTx = measurement * 60 * 60
111
135
  if Double(transmission) > maxTx {
112
136
  throw TempivoSensorError(.invalidConfig, "transmissionIntervalSeconds must be at most 60 times the measurement interval.")
@@ -6,6 +6,23 @@ public struct TempivoSensorQr: Sendable, Equatable {
6
6
  public let model: String?
7
7
  public let sessionType: TempivoSensorSessionType
8
8
  public let bluetoothMac: String
9
+ public let deviceId: String?
10
+
11
+ public init(
12
+ serial: String,
13
+ pin: String,
14
+ model: String?,
15
+ sessionType: TempivoSensorSessionType,
16
+ bluetoothMac: String,
17
+ deviceId: String? = nil
18
+ ) {
19
+ self.serial = serial
20
+ self.pin = pin
21
+ self.model = model
22
+ self.sessionType = sessionType
23
+ self.bluetoothMac = bluetoothMac
24
+ self.deviceId = deviceId
25
+ }
9
26
  }
10
27
 
11
28
  public enum TempivoSensorQrParser {
@@ -27,6 +44,14 @@ public enum TempivoSensorQrParser {
27
44
  }.joined(separator: ":")
28
45
  }
29
46
 
47
+ public static func normalizeMac(_ value: String) throws -> String {
48
+ let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
49
+ if trimmed.contains(":") {
50
+ return try bluetoothMacFromSerial(trimmed.replacingOccurrences(of: ":", with: ""))
51
+ }
52
+ return try bluetoothMacFromSerial(trimmed)
53
+ }
54
+
30
55
  public static let defaultModel = "HC7"
31
56
 
32
57
  public static func assertSupportedModel(_ model: String) throws {
@@ -63,12 +88,24 @@ public enum TempivoSensorQrParser {
63
88
  let model = rawModel.isEmpty ? defaultModel : rawModel
64
89
  try assertSupportedModel(model)
65
90
  let hex = serial.count == 12 ? serial : String(serial.suffix(12))
91
+ let macOverride =
92
+ (["bluetoothMac", "bluetoothMacAddress", "mac"] as [String])
93
+ .compactMap { key -> String? in
94
+ guard let raw = obj[key] as? String else { return nil }
95
+ let t = raw.trimmingCharacters(in: .whitespacesAndNewlines)
96
+ return t.isEmpty ? nil : t
97
+ }
98
+ .first
99
+ let deviceIdRaw = (obj["deviceId"] as? String)?
100
+ .trimmingCharacters(in: .whitespacesAndNewlines)
101
+ let deviceId = (deviceIdRaw?.isEmpty == false) ? deviceIdRaw : nil
66
102
  return TempivoSensorQr(
67
103
  serial: serial,
68
104
  pin: pin,
69
105
  model: model,
70
106
  sessionType: .modern,
71
- bluetoothMac: try bluetoothMacFromSerial(hex)
107
+ bluetoothMac: try macOverride.map(normalizeMac) ?? bluetoothMacFromSerial(hex),
108
+ deviceId: deviceId
72
109
  )
73
110
  }
74
111
 
@@ -15,22 +15,30 @@ public final class TempivoSensorSession: @unchecked Sendable {
15
15
 
16
16
  public init() {}
17
17
 
18
- public func connect(serial: String, bluetoothMac: String, pin: String) throws {
19
- guard let pinInt = Int32(pin.trimmingCharacters(in: .whitespacesAndNewlines)) else {
18
+ public func connect(
19
+ serial: String,
20
+ bluetoothMac: String,
21
+ pin: String,
22
+ deviceId: String? = nil
23
+ ) throws {
24
+ let pinValue = pin.trimmingCharacters(in: .whitespacesAndNewlines)
25
+ guard !pinValue.isEmpty, pinValue.range(of: "^[0-9]+$", options: .regularExpression) != nil else {
20
26
  throw TempivoSensorError(.invalidPin, "PIN must be numeric.")
21
27
  }
28
+ let trimmedDeviceId = deviceId?.trimmingCharacters(in: .whitespacesAndNewlines)
29
+ let resolvedDeviceId =
30
+ (trimmedDeviceId?.isEmpty == false ? trimmedDeviceId : nil)
31
+ ?? TempivoSensorQrParser.normalizeSerial(serial)
22
32
  #if canImport(TempivoSensorBridge)
23
33
  lock.lock()
24
34
  defer { lock.unlock() }
25
35
  bridge.initialize()
26
36
  let result = parseBridgeJson(
27
37
  bridge.connectPayloadJson(
28
- serial: TempivoSensorQrParser.normalizeSerial(serial),
38
+ deviceId: resolvedDeviceId,
29
39
  bluetoothMac: bluetoothMac,
30
- pin: pinInt,
31
- legacy: false,
32
- deviceId: TempivoSensorQrParser.normalizeSerial(serial),
33
- encryptionKey: ""
40
+ pin: pinValue,
41
+ legacy: false
34
42
  )
35
43
  )
36
44
  try throwIfBridgeError(result)
@@ -40,8 +48,13 @@ public final class TempivoSensorSession: @unchecked Sendable {
40
48
  #endif
41
49
  }
42
50
 
43
- public func connect(qr: TempivoSensorQr) throws {
44
- try connect(serial: qr.serial, bluetoothMac: qr.bluetoothMac, pin: qr.pin)
51
+ public func connect(qr: TempivoSensorQr, deviceId: String? = nil, bluetoothMac: String? = nil) throws {
52
+ try connect(
53
+ serial: qr.serial,
54
+ bluetoothMac: bluetoothMac ?? qr.bluetoothMac,
55
+ pin: qr.pin,
56
+ deviceId: deviceId ?? qr.deviceId
57
+ )
45
58
  }
46
59
 
47
60
  public func disconnect() {
@@ -58,7 +71,7 @@ public final class TempivoSensorSession: @unchecked Sendable {
58
71
  }
59
72
 
60
73
  public func getConfiguration() throws -> TempivoSensorConfiguration {
61
- try TempivoSensorProfile.parse(json: getConfigurationJson())
74
+ try TempivoSensorProfile.parseFromDevice(json: getConfigurationJson())
62
75
  }
63
76
 
64
77
  public func getConfigurationJson() throws -> String {
@@ -98,8 +111,7 @@ public final class TempivoSensorSession: @unchecked Sendable {
98
111
  try throwIfBridgeError(result)
99
112
  }
100
113
  return TempivoTriggerTransmissionResult(
101
- ok: (result["ok"] as? Bool) ?? false,
102
- supported: (result["supported"] as? Bool) ?? false
114
+ ok: (result["ok"] as? Bool) ?? false
103
115
  )
104
116
  #else
105
117
  throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
@@ -92,10 +92,8 @@ public struct TempivoSensorCalibration: Sendable, Equatable {
92
92
 
93
93
  public struct TempivoTriggerTransmissionResult: Sendable, Equatable {
94
94
  public var ok: Bool
95
- public var supported: Bool
96
95
 
97
- public init(ok: Bool, supported: Bool) {
96
+ public init(ok: Bool) {
98
97
  self.ok = ok
99
- self.supported = supported
100
98
  }
101
99
  }