@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,24 @@
1
+ import Foundation
2
+
3
+ public enum TempivoSensorErrorCode: String, Sendable {
4
+ case invalidPin
5
+ case invalidQr
6
+ case invalidConfig
7
+ case notConnected
8
+ case unsupportedCommand
9
+ case runtimeUnavailable
10
+ case connectFailed
11
+ case unknown
12
+ }
13
+
14
+ public struct TempivoSensorError: Error, LocalizedError, Sendable {
15
+ public let code: TempivoSensorErrorCode
16
+ public let message: String
17
+
18
+ public init(_ code: TempivoSensorErrorCode, _ message: String) {
19
+ self.code = code
20
+ self.message = message
21
+ }
22
+
23
+ public var errorDescription: String? { message }
24
+ }
@@ -0,0 +1,123 @@
1
+ import Foundation
2
+
3
+ public enum TempivoSensorProfile {
4
+ public static func parse(json: String) throws -> TempivoSensorConfiguration {
5
+ guard let data = json.data(using: .utf8),
6
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
7
+ else {
8
+ throw TempivoSensorError(.invalidConfig, "Configuration is not valid JSON.")
9
+ }
10
+ return try parse(obj)
11
+ }
12
+
13
+ public static func parse(_ obj: [String: Any]) throws -> TempivoSensorConfiguration {
14
+ if obj["profiles"] is [Any] {
15
+ throw TempivoSensorError(.invalidConfig, "Pass one profile from GET /devices/config-profiles/{slug}, not the list.")
16
+ }
17
+ let body: [String: Any]
18
+ if let nested = obj["profile"] as? [String: Any],
19
+ nested["temperatureAlerts"] != nil || nested["schedule"] != nil || nested["slug"] != nil {
20
+ body = nested
21
+ } else {
22
+ body = obj
23
+ }
24
+ if body["temperatureAlerts"] == nil && body["alarmRules"] != nil {
25
+ throw TempivoSensorError(.invalidConfig, "Use a config profile with temperatureAlerts (GET /devices/config-profiles/{slug}), not alarmRules.")
26
+ }
27
+ let rawAlerts = body["temperatureAlerts"] as? [Any] ?? []
28
+ let alerts = try rawAlerts.enumerated().map { try parseAlert($0.element, index: $0.offset) }
29
+ return TempivoSensorConfiguration(temperatureAlerts: alerts, schedule: try parseSchedule(body["schedule"] as? [String: Any]))
30
+ }
31
+
32
+ public static func jsonString(_ cfg: TempivoSensorConfiguration) throws -> String {
33
+ var alerts: [[String: Any]] = []
34
+ for a in cfg.temperatureAlerts {
35
+ var o: [String: Any] = [
36
+ "type": a.type,
37
+ "channel": a.channel.rawValue,
38
+ "transmitOnBreach": a.transmitOnBreach,
39
+ ]
40
+ if let v = a.lowC { o["lowC"] = v }
41
+ if let v = a.highC { o["highC"] = v }
42
+ if let v = a.minC { o["minC"] = v }
43
+ if let v = a.maxC { o["maxC"] = v }
44
+ if let v = a.hysteresisC { o["hysteresisC"] = v }
45
+ if a.type == "range" { o["transmitOnReturn"] = a.transmitOnReturn }
46
+ alerts.append(o)
47
+ }
48
+ var schedule: [String: Any]
49
+ if cfg.schedule.always {
50
+ schedule = ["always": true]
51
+ } else {
52
+ schedule = [
53
+ "weekdays": cfg.schedule.weekdays,
54
+ "from": cfg.schedule.from ?? "00:00",
55
+ "to": cfg.schedule.to ?? "23:59",
56
+ "utcOffsetMinutes": cfg.schedule.utcOffsetMinutes,
57
+ ]
58
+ }
59
+ let root: [String: Any] = ["temperatureAlerts": alerts, "schedule": schedule]
60
+ let data = try JSONSerialization.data(withJSONObject: root, options: [])
61
+ return String(data: data, encoding: .utf8) ?? "{}"
62
+ }
63
+
64
+ private static func parseAlert(_ raw: Any, index: Int) throws -> TempivoTemperatureAlert {
65
+ guard let item = raw as? [String: Any] else {
66
+ throw TempivoSensorError(.invalidConfig, "temperatureAlerts[\(index)] must be an object.")
67
+ }
68
+ guard let channelRaw = item["channel"] as? String,
69
+ let channel = TempivoTemperatureChannel(rawValue: channelRaw)
70
+ else {
71
+ throw TempivoSensorError(.invalidConfig, "temperatureAlerts[\(index)].channel must be ambient or probe.")
72
+ }
73
+ let hyst = number(item["hysteresisC"]) ?? 1
74
+ let transmit = (item["transmitOnBreach"] as? Bool) ?? true
75
+ let transmitOnReturn = (item["transmitOnReturn"] as? Bool) ?? true
76
+ switch item["type"] as? String {
77
+ case "range":
78
+ guard let low = number(item["lowC"]), let high = number(item["highC"]) else {
79
+ throw TempivoSensorError(.invalidConfig, "temperatureAlerts[\(index)] needs lowC and highC.")
80
+ }
81
+ guard high > low else {
82
+ throw TempivoSensorError(.invalidConfig, "temperatureAlerts[\(index)]: highC must be greater than lowC.")
83
+ }
84
+ return TempivoTemperatureAlert(type: "range", channel: channel, lowC: low, highC: high, hysteresisC: hyst, transmitOnBreach: transmit, transmitOnReturn: transmitOnReturn)
85
+ case "min":
86
+ guard let minC = number(item["minC"]) else {
87
+ throw TempivoSensorError(.invalidConfig, "temperatureAlerts[\(index)] needs minC.")
88
+ }
89
+ return TempivoTemperatureAlert(type: "min", channel: channel, minC: minC, hysteresisC: hyst, transmitOnBreach: transmit)
90
+ case "max":
91
+ guard let maxC = number(item["maxC"]) else {
92
+ throw TempivoSensorError(.invalidConfig, "temperatureAlerts[\(index)] needs maxC.")
93
+ }
94
+ return TempivoTemperatureAlert(type: "max", channel: channel, maxC: maxC, hysteresisC: hyst, transmitOnBreach: transmit)
95
+ default:
96
+ throw TempivoSensorError(.invalidConfig, "temperatureAlerts[\(index)].type must be range, min, or max.")
97
+ }
98
+ }
99
+
100
+ private static func parseSchedule(_ raw: [String: Any]?) throws -> TempivoSchedule {
101
+ guard let raw else { return .alwaysOn }
102
+ if raw["always"] as? Bool == true { return .alwaysOn }
103
+ guard let weekdays = raw["weekdays"] as? [Int], !weekdays.isEmpty, weekdays.allSatisfy({ (0...6).contains($0) }) else {
104
+ throw TempivoSensorError(.invalidConfig, "schedule.weekdays is required.")
105
+ }
106
+ guard let from = raw["from"] as? String, let to = raw["to"] as? String,
107
+ from.range(of: "^([01]\\d|2[0-3]):([0-5]\\d)$", options: .regularExpression) != nil,
108
+ to.range(of: "^([01]\\d|2[0-3]):([0-5]\\d)$", options: .regularExpression) != nil
109
+ else {
110
+ throw TempivoSensorError(.invalidConfig, "schedule.from and schedule.to must be HH:MM.")
111
+ }
112
+ guard let utc = raw["utcOffsetMinutes"] as? Int else {
113
+ throw TempivoSensorError(.invalidConfig, "schedule.utcOffsetMinutes is required.")
114
+ }
115
+ return TempivoSchedule(always: false, weekdays: weekdays, from: from, to: to, utcOffsetMinutes: utc)
116
+ }
117
+
118
+ private static func number(_ value: Any?) -> Double? {
119
+ if let d = value as? Double { return d }
120
+ if let n = value as? NSNumber { return n.doubleValue }
121
+ return nil
122
+ }
123
+ }
@@ -0,0 +1,78 @@
1
+ import Foundation
2
+
3
+ public struct TempivoSensorQr: Sendable, Equatable {
4
+ public let serial: String
5
+ public let pin: String
6
+ public let model: String?
7
+ public let sessionType: TempivoSensorSessionType
8
+ public let bluetoothMac: String
9
+ }
10
+
11
+ public enum TempivoSensorQrParser {
12
+ public static func normalizeSerial(_ value: String) -> String {
13
+ value.trimmingCharacters(in: .whitespacesAndNewlines)
14
+ .uppercased()
15
+ .replacingOccurrences(of: "[^A-Z0-9]", with: "", options: .regularExpression)
16
+ }
17
+
18
+ public static func bluetoothMacFromSerial(_ serial: String) throws -> String {
19
+ let key = normalizeSerial(serial)
20
+ guard key.count == 12, key.range(of: "^[0-9A-F]{12}$", options: .regularExpression) != nil else {
21
+ throw TempivoSensorError(.invalidQr, "Serial must be 12 hex characters.")
22
+ }
23
+ return stride(from: 0, to: 12, by: 2).map { i in
24
+ let start = key.index(key.startIndex, offsetBy: i)
25
+ let end = key.index(start, offsetBy: 2)
26
+ return String(key[start..<end])
27
+ }.joined(separator: ":")
28
+ }
29
+
30
+ public static let defaultModel = "HC7"
31
+
32
+ public static func sessionType(fromModel model: String?) -> TempivoSensorSessionType {
33
+ guard let model, !model.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
34
+ return .modern
35
+ }
36
+ let m = model.trimmingCharacters(in: .whitespacesAndNewlines).uppercased().replacingOccurrences(of: "-", with: "")
37
+ if m == "HC5" || m.hasPrefix("6") { return .legacy }
38
+ return .modern
39
+ }
40
+
41
+ public static func parse(_ json: String) throws -> TempivoSensorQr {
42
+ guard let data = json.trimmingCharacters(in: .whitespacesAndNewlines).data(using: .utf8),
43
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
44
+ else {
45
+ throw TempivoSensorError(.invalidQr, "QR is not valid JSON.")
46
+ }
47
+ let snRaw: String = {
48
+ if let s = obj["sn"] as? String { return s }
49
+ if let n = obj["sn"] as? NSNumber { return n.stringValue }
50
+ return ""
51
+ }()
52
+ let serial = normalizeSerial(snRaw)
53
+ let pin = pinFrom(obj)
54
+ guard serial.count >= 8 else {
55
+ throw TempivoSensorError(.invalidQr, "QR is missing serial (sn).")
56
+ }
57
+ guard !pin.isEmpty else {
58
+ throw TempivoSensorError(.invalidQr, "QR is missing PIN.")
59
+ }
60
+ let rawModel = (obj["model"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
61
+ let model = rawModel.isEmpty ? defaultModel : rawModel
62
+ let hex = serial.count == 12 ? serial : String(serial.suffix(12))
63
+ return TempivoSensorQr(
64
+ serial: serial,
65
+ pin: pin,
66
+ model: model,
67
+ sessionType: sessionType(fromModel: model),
68
+ bluetoothMac: try bluetoothMacFromSerial(hex)
69
+ )
70
+ }
71
+
72
+ private static func pinFrom(_ obj: [String: Any]) -> String {
73
+ let raw = obj["pin"] ?? obj["resetCode"]
74
+ if let s = raw as? String { return s.trimmingCharacters(in: .whitespacesAndNewlines) }
75
+ if let n = raw as? NSNumber { return n.stringValue }
76
+ return ""
77
+ }
78
+ }
@@ -0,0 +1,161 @@
1
+ import Foundation
2
+
3
+ #if canImport(TempivoSensorBridge)
4
+ import TempivoSensorBridge
5
+ #endif
6
+
7
+ /// GATT session: connect, read/write alert rules and schedule, trigger uplink, read lab calibration date.
8
+ public final class TempivoSensorSession: @unchecked Sendable {
9
+ private let lock = NSLock()
10
+ private var connected = false
11
+
12
+ #if canImport(TempivoSensorBridge)
13
+ private let bridge = TempivoSensorBridge()
14
+ #endif
15
+
16
+ public init() {}
17
+
18
+ public func connect(serial: String, bluetoothMac: String, pin: String, sessionType: TempivoSensorSessionType = .modern) throws {
19
+ guard let pinInt = Int32(pin.trimmingCharacters(in: .whitespacesAndNewlines)) else {
20
+ throw TempivoSensorError(.invalidPin, "PIN must be numeric.")
21
+ }
22
+ #if canImport(TempivoSensorBridge)
23
+ lock.lock()
24
+ defer { lock.unlock() }
25
+ bridge.initialize()
26
+ let result = parseBridgeJson(
27
+ bridge.connectPayloadJson(
28
+ serial: TempivoSensorQrParser.normalizeSerial(serial),
29
+ bluetoothMac: bluetoothMac,
30
+ pin: pinInt,
31
+ legacy: sessionType == .legacy
32
+ )
33
+ )
34
+ try throwIfBridgeError(result)
35
+ connected = true
36
+ #else
37
+ throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
38
+ #endif
39
+ }
40
+
41
+ public func connect(qr: TempivoSensorQr) throws {
42
+ try connect(serial: qr.serial, bluetoothMac: qr.bluetoothMac, pin: qr.pin, sessionType: qr.sessionType)
43
+ }
44
+
45
+ public func disconnect() {
46
+ #if canImport(TempivoSensorBridge)
47
+ lock.lock()
48
+ defer { lock.unlock() }
49
+ bridge.disconnect()
50
+ connected = false
51
+ #else
52
+ lock.lock()
53
+ connected = false
54
+ lock.unlock()
55
+ #endif
56
+ }
57
+
58
+ public func getConfiguration() throws -> TempivoSensorConfiguration {
59
+ try TempivoSensorProfile.parse(json: getConfigurationJson())
60
+ }
61
+
62
+ public func getConfigurationJson() throws -> String {
63
+ #if canImport(TempivoSensorBridge)
64
+ try requireConnected()
65
+ let result = parseBridgeJson(bridge.getConfigurationPayloadJson())
66
+ try throwIfBridgeError(result)
67
+ guard let json = result["configurationJson"] as? String else {
68
+ throw TempivoSensorError(.unknown, "Could not read configuration.")
69
+ }
70
+ return json
71
+ #else
72
+ throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
73
+ #endif
74
+ }
75
+
76
+ public func setConfiguration(_ configuration: TempivoSensorConfiguration) throws {
77
+ try setConfigurationJson(TempivoSensorProfile.jsonString(configuration))
78
+ }
79
+
80
+ public func setConfigurationJson(_ json: String) throws {
81
+ _ = try TempivoSensorProfile.parse(json: json)
82
+ #if canImport(TempivoSensorBridge)
83
+ try requireConnected()
84
+ let result = parseBridgeJson(bridge.setConfigurationPayloadJson(json: json))
85
+ try throwIfBridgeError(result)
86
+ #else
87
+ throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
88
+ #endif
89
+ }
90
+
91
+ public func triggerTransmission() throws -> TempivoTriggerTransmissionResult {
92
+ #if canImport(TempivoSensorBridge)
93
+ try requireConnected()
94
+ let result = parseBridgeJson(bridge.triggerTransmissionPayloadJson())
95
+ if let code = intValue(result["errorCode"]), code != 0 {
96
+ try throwIfBridgeError(result)
97
+ }
98
+ return TempivoTriggerTransmissionResult(
99
+ ok: (result["ok"] as? Bool) ?? false,
100
+ supported: (result["supported"] as? Bool) ?? false
101
+ )
102
+ #else
103
+ throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
104
+ #endif
105
+ }
106
+
107
+ public func getCalibration() throws -> TempivoSensorCalibration {
108
+ #if canImport(TempivoSensorBridge)
109
+ try requireConnected()
110
+ let result = parseBridgeJson(bridge.getCalibrationPayloadJson())
111
+ try throwIfBridgeError(result)
112
+ let ts = int64Value(result["laboratoryCalibrationTimestamp"])
113
+ return TempivoSensorCalibrationDecoder.decode(ts)
114
+ #else
115
+ throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT requires a physical device with the native runtime.")
116
+ #endif
117
+ }
118
+
119
+ private func requireConnected() throws {
120
+ lock.lock()
121
+ let ok = connected
122
+ lock.unlock()
123
+ if !ok {
124
+ throw TempivoSensorError(.notConnected, "Not connected.")
125
+ }
126
+ }
127
+
128
+ private func parseBridgeJson(_ raw: String) -> [String: Any] {
129
+ guard let data = raw.data(using: .utf8),
130
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
131
+ else {
132
+ return ["errorCode": 7]
133
+ }
134
+ return obj
135
+ }
136
+
137
+ private func intValue(_ raw: Any?) -> Int? {
138
+ if let i = raw as? Int { return i }
139
+ if let n = raw as? NSNumber { return n.intValue }
140
+ return nil
141
+ }
142
+
143
+ private func int64Value(_ raw: Any?) -> Int64? {
144
+ if let i = raw as? Int64 { return i }
145
+ if let n = raw as? NSNumber { return n.int64Value }
146
+ return nil
147
+ }
148
+
149
+ private func throwIfBridgeError(_ result: [String: Any]) throws {
150
+ guard let code = intValue(result["errorCode"]), code != 0 else { return }
151
+ switch code {
152
+ case 1: throw TempivoSensorError(.invalidPin, "Invalid PIN.")
153
+ case 2: throw TempivoSensorError(.notConnected, "Not connected.")
154
+ case 3: throw TempivoSensorError(.unsupportedCommand, "Unsupported command.")
155
+ case 4: throw TempivoSensorError(.runtimeUnavailable, "Sensor GATT is not available on this device.")
156
+ case 5: throw TempivoSensorError(.connectFailed, "Could not connect.")
157
+ case 6: throw TempivoSensorError(.invalidConfig, (result["message"] as? String) ?? "Invalid configuration.")
158
+ default: throw TempivoSensorError(.unknown, "Sensor request failed.")
159
+ }
160
+ }
161
+ }
@@ -0,0 +1,93 @@
1
+ import Foundation
2
+
3
+ public enum TempivoSensorSessionType: String, Sendable {
4
+ case modern
5
+ case legacy
6
+ }
7
+
8
+ public enum TempivoTemperatureChannel: String, Sendable {
9
+ case ambient
10
+ case probe
11
+ }
12
+
13
+ public struct TempivoTemperatureAlert: Sendable, Equatable {
14
+ public var type: String
15
+ public var channel: TempivoTemperatureChannel
16
+ public var lowC: Double?
17
+ public var highC: Double?
18
+ public var minC: Double?
19
+ public var maxC: Double?
20
+ public var hysteresisC: Double?
21
+ public var transmitOnBreach: Bool
22
+ public var transmitOnReturn: Bool
23
+
24
+ public init(
25
+ type: String,
26
+ channel: TempivoTemperatureChannel,
27
+ lowC: Double? = nil,
28
+ highC: Double? = nil,
29
+ minC: Double? = nil,
30
+ maxC: Double? = nil,
31
+ hysteresisC: Double? = nil,
32
+ transmitOnBreach: Bool = true,
33
+ transmitOnReturn: Bool = true
34
+ ) {
35
+ self.type = type
36
+ self.channel = channel
37
+ self.lowC = lowC
38
+ self.highC = highC
39
+ self.minC = minC
40
+ self.maxC = maxC
41
+ self.hysteresisC = hysteresisC
42
+ self.transmitOnBreach = transmitOnBreach
43
+ self.transmitOnReturn = transmitOnReturn
44
+ }
45
+ }
46
+
47
+ public struct TempivoSchedule: Sendable, Equatable {
48
+ public var always: Bool
49
+ public var weekdays: [Int]
50
+ public var from: String?
51
+ public var to: String?
52
+ public var utcOffsetMinutes: Int
53
+
54
+ public static let alwaysOn = TempivoSchedule(always: true, weekdays: [], from: nil, to: nil, utcOffsetMinutes: 0)
55
+
56
+ public init(always: Bool, weekdays: [Int] = [], from: String? = nil, to: String? = nil, utcOffsetMinutes: Int = 0) {
57
+ self.always = always
58
+ self.weekdays = weekdays
59
+ self.from = from
60
+ self.to = to
61
+ self.utcOffsetMinutes = utcOffsetMinutes
62
+ }
63
+ }
64
+
65
+ public struct TempivoSensorConfiguration: Sendable, Equatable {
66
+ public var temperatureAlerts: [TempivoTemperatureAlert]
67
+ public var schedule: TempivoSchedule
68
+
69
+ public init(temperatureAlerts: [TempivoTemperatureAlert], schedule: TempivoSchedule) {
70
+ self.temperatureAlerts = temperatureAlerts
71
+ self.schedule = schedule
72
+ }
73
+ }
74
+
75
+ public struct TempivoSensorCalibration: Sendable, Equatable {
76
+ public var laboratoryCalibrationDate: String?
77
+ public var laboratoryCalibrationTimestamp: Int64?
78
+
79
+ public init(laboratoryCalibrationDate: String?, laboratoryCalibrationTimestamp: Int64?) {
80
+ self.laboratoryCalibrationDate = laboratoryCalibrationDate
81
+ self.laboratoryCalibrationTimestamp = laboratoryCalibrationTimestamp
82
+ }
83
+ }
84
+
85
+ public struct TempivoTriggerTransmissionResult: Sendable, Equatable {
86
+ public var ok: Bool
87
+ public var supported: Bool
88
+
89
+ public init(ok: Bool, supported: Bool) {
90
+ self.ok = ok
91
+ self.supported = supported
92
+ }
93
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tempivo/sensor-beacon",
3
- "version": "0.1.0",
4
- "description": "Decode Tempivo sensor BLE manufacturer advertisements (0x026C). Advertising only, no GATT connect. For integrators and custom apps.",
3
+ "version": "0.2.0",
4
+ "description": "Tempivo sensor BLE SDK: advertisement decode plus GATT session (config rules, transmission trigger, lab calibration date).",
5
5
  "homepage": "https://app.tempivo.com/integrations",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -14,6 +14,8 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
+ "android",
18
+ "ios",
17
19
  "LICENSE",
18
20
  "README.md"
19
21
  ],
@@ -24,17 +26,29 @@
24
26
  "node": ">=18"
25
27
  },
26
28
  "scripts": {
27
- "build": "tsc",
28
- "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.mjs",
29
- "prepublishOnly": "npm run build"
29
+ "build": "npm run build:ts",
30
+ "build:ts": "tsc",
31
+ "build:android": "node scripts/build-aar.mjs",
32
+ "build:ios-bridge": "bash scripts/build-ios-bridge.sh",
33
+ "build:all": "npm run build:ts && npm run build:android",
34
+ "prepare": "npm run build:ts",
35
+ "test": "npm run test:all",
36
+ "test:unit": "node --experimental-vm-modules node_modules/jest/bin/jest.js --config jest.config.mjs",
37
+ "test:python": "PYTHONPATH=python python3 -m pytest python/tests -q",
38
+ "test:all": "npm run test:unit && npm run test:python",
39
+ "test:smoke": "node scripts/smoke-test.mjs",
40
+ "prepublishOnly": "npm run build:all"
30
41
  },
31
42
  "keywords": [
32
43
  "tempivo",
33
44
  "ble",
34
45
  "bluetooth",
35
46
  "beacon",
47
+ "gatt",
36
48
  "sensor",
37
- "manufacturer-data"
49
+ "manufacturer-data",
50
+ "android",
51
+ "ios"
38
52
  ],
39
53
  "license": "MIT",
40
54
  "devDependencies": {