@whois-homebridge/homebridge-aranet4 0.1.2

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 ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (Unreleased)
4
+
5
+ Initial release.
6
+
7
+ - BLE communication with Aranet4 devices via `@homebridge/noble`
8
+ - HomeKit services: CO2, temperature, humidity, air quality (CO2-derived), battery, atmospheric pressure (Eve custom)
9
+ - Local SQLite data persistence with configurable retention
10
+ - Eve app historical graphs via fakegato-history
11
+ - Device onboard history sync (opt-in, requires BLE pairing)
12
+ - Multi-device auto-discovery and manual MAC address configuration
13
+ - Exponential backoff reconnection and robust error handling
14
+ - Cross-platform: macOS (Core Bluetooth) and Linux/Raspberry Pi (BlueZ)
package/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # homebridge-aranet4-advanced
2
+
3
+ Homebridge plugin for the [Aranet4](https://aranet.com/products/aranet4/) CO2 and environment sensor. Exposes CO2, temperature, humidity, atmospheric pressure, air quality, and battery level to Apple HomeKit via passive BLE advertisement scanning.
4
+
5
+ Supports [Eve](https://apps.apple.com/app/eve-for-matter-homekit/id917695792) app history graphs via FakeGato.
6
+
7
+ ## Features
8
+
9
+ - **Passive BLE scanning** -- reads sensor data from Aranet4 advertisements without connecting, minimizing battery drain and BLE contention
10
+ - **Minimal dependencies** -- passive BLE scanning, no database or heavy native modules required
11
+ - **Multi-device support** -- monitor multiple Aranet4 sensors simultaneously
12
+ - **Auto-discovery** -- finds Aranet4 devices in range automatically (or configure explicit MAC addresses)
13
+ - **Eve history** -- temperature, humidity, and CO2 graphs in the Eve app
14
+ - **HomeKit services**: CO2 Sensor, Temperature, Humidity, Air Quality, Atmospheric Pressure (Eve-compatible), Battery
15
+
16
+ ## Prerequisites
17
+
18
+ - Homebridge v1.8+ or v2.0+
19
+ - Node.js 20 or 22
20
+ - A Bluetooth adapter (built-in or USB) supported by your OS
21
+ - Aranet4 with **"Smart Home integrations"** enabled in the Aranet4 Home app
22
+
23
+ ### Enabling Smart Home Integrations
24
+
25
+ The Aranet4 only broadcasts sensor data in BLE advertisements when this setting is enabled:
26
+
27
+ 1. Open the **Aranet4 Home** app on your phone
28
+ 2. Connect to your Aranet4 device
29
+ 3. Go to **Settings** > **Smart Home integrations**
30
+ 4. Toggle it **on**
31
+
32
+ Without this, the plugin will detect the device by name but won't receive sensor data.
33
+
34
+ ## Installation
35
+
36
+ Install via the Homebridge UI (search for `homebridge-aranet4-advanced`) or manually:
37
+
38
+ ```bash
39
+ npm install -g homebridge-aranet4-advanced
40
+ ```
41
+
42
+ ### Bluetooth Permissions
43
+
44
+ **macOS** -- Bluetooth works out of the box via Core Bluetooth. If running Homebridge via Terminal, grant Terminal Bluetooth permission in System Settings > Privacy & Security > Bluetooth.
45
+
46
+ **Linux / Raspberry Pi** -- Install BlueZ and grant the Node.js binary BLE capabilities:
47
+
48
+ ```bash
49
+ sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
50
+ sudo setcap cap_net_raw+eip $(eval readlink -f $(which node))
51
+ ```
52
+
53
+ ## Configuration
54
+
55
+ ### Minimal (auto-discovery)
56
+
57
+ ```json
58
+ {
59
+ "platforms": [
60
+ {
61
+ "platform": "Aranet4",
62
+ "name": "Aranet4"
63
+ }
64
+ ]
65
+ }
66
+ ```
67
+
68
+ The plugin will discover any Aranet4 in range and create accessories automatically.
69
+
70
+ ### Explicit device configuration
71
+
72
+ ```json
73
+ {
74
+ "platforms": [
75
+ {
76
+ "platform": "Aranet4",
77
+ "name": "Aranet4",
78
+ "devices": [
79
+ {
80
+ "name": "Living Room CO2",
81
+ "address": "AA:BB:CC:DD:EE:FF",
82
+ "pollingInterval": 120,
83
+ "co2AlertThreshold": 1000,
84
+ "lowBatteryThreshold": 15,
85
+ "enableHistory": true
86
+ }
87
+ ]
88
+ }
89
+ ]
90
+ }
91
+ ```
92
+
93
+ ### Device options
94
+
95
+ | Option | Type | Default | Description |
96
+ |--------|------|---------|-------------|
97
+ | `name` | string | `"Aranet4"` | Friendly name shown in HomeKit |
98
+ | `address` | string | *(auto)* | Bluetooth MAC address (`AA:BB:CC:DD:EE:FF`). Recommended for multi-device setups. |
99
+ | `pollingInterval` | integer | `60` | Seconds between HomeKit updates (60--3600). The Aranet4 broadcasts every ~1s; this throttles how often the plugin pushes updates. |
100
+ | `co2AlertThreshold` | integer | `1000` | CO2 ppm level that triggers `CarbonDioxideDetected` in HomeKit (400--5000). |
101
+ | `lowBatteryThreshold` | integer | `15` | Battery % below which low-battery alert triggers (5--50). |
102
+ | `enableHistory` | boolean | `true` | Enable Eve app history graphs (via FakeGato). |
103
+
104
+ ### Finding your Aranet4 MAC address
105
+
106
+ The plugin logs the MAC address of each discovered device at startup. Check your Homebridge logs for:
107
+
108
+ ```
109
+ [Aranet4] Discovered Aranet4: "Aranet4 Home" [aabbccddeeff] via advertisement
110
+ ```
111
+
112
+ Use that address (formatted as `AA:BB:CC:DD:EE:FF`) in your config.
113
+
114
+ ## HomeKit Services
115
+
116
+ Each Aranet4 device exposes these services:
117
+
118
+ | Sensor Data | HomeKit Service | Details |
119
+ |-------------|----------------|---------|
120
+ | CO2 (ppm) | CarbonDioxideSensor | Level + alert at configurable threshold |
121
+ | Temperature (C) | TemperatureSensor | |
122
+ | Humidity (%) | HumiditySensor | |
123
+ | Air Quality | AirQualitySensor | Derived from CO2 level (see below) |
124
+ | Pressure (hPa) | Eve custom characteristic | Visible in the Eve app |
125
+ | Battery (%) | BatteryService | Low-battery alert at configurable threshold |
126
+
127
+ ### Air Quality Mapping
128
+
129
+ | CO2 (ppm) | Air Quality |
130
+ |-----------|-------------|
131
+ | 0--600 | Excellent |
132
+ | 601--800 | Good |
133
+ | 801--1000 | Fair |
134
+ | 1001--1500 | Inferior |
135
+ | >1500 | Poor |
136
+
137
+ ## Troubleshooting
138
+
139
+ ### "No sensor data in advertisement"
140
+
141
+ The Aranet4 was detected by name but isn't broadcasting sensor data. Enable **Smart Home integrations** in the Aranet4 Home app (see Prerequisites above).
142
+
143
+ ### Plugin starts but no devices found
144
+
145
+ - Verify your Bluetooth adapter is working: `hciconfig` (Linux) or check System Settings (macOS)
146
+ - Check Homebridge logs for `Bluetooth adapter state: poweredOn`
147
+ - Ensure no other process is monopolizing the BLE adapter
148
+ - On Linux, verify BLE capabilities are set (see Bluetooth Permissions above)
149
+
150
+ ### Readings appear stale or intermittent
151
+
152
+ - The Aranet4 measures every 1--10 minutes (configurable on device). Between measurements, it broadcasts the last known reading.
153
+ - BLE range is typically 10--20m. Move the Homebridge host closer to the sensor.
154
+ - Other BLE devices can cause interference. A dedicated USB Bluetooth adapter may help.
155
+
156
+ ### "Bluetooth access denied" (macOS)
157
+
158
+ On macOS, Bluetooth requires explicit permission per executable. The plugin logs a clear error when this happens. This affects both the main bridge and child bridge setups.
159
+
160
+ **Fix — grant Bluetooth permission to the Node.js binary:**
161
+
162
+ 1. Open **System Settings → Privacy & Security → Bluetooth**
163
+ 2. Click the **+** button to add an application
164
+ 3. Press **Cmd+Shift+G** to open the path input dialog
165
+ 4. Type the path to your Node.js binary and press Enter:
166
+ - Homebrew: `/opt/homebrew/bin/node` (Apple Silicon) or `/usr/local/bin/node` (Intel)
167
+ - hb-service default: check the path shown in `sudo hb-service status` or in your Homebridge startup logs (look for the `Node.js` line)
168
+ 5. Select the binary and click **Open**
169
+ 6. Make sure the toggle next to `node` is **ON**
170
+ 7. Restart Homebridge
171
+
172
+ This works for both the main bridge and child bridges because Homebridge child bridges use the same `node` binary (via `child_process.fork()`), and macOS tracks Bluetooth permission by executable path.
173
+
174
+ **Caveats:**
175
+ - If you update Node.js (e.g., via Homebrew), the binary path may change and you'll need to re-add it
176
+ - If `node` is a symlink, you may need to add the resolved path instead (e.g., `/opt/homebrew/Cellar/node/24.14.1/bin/node`)
177
+
178
+ ### "BLE scan start failed"
179
+
180
+ Make sure Bluetooth is enabled and the Node.js process has the necessary permissions (see Bluetooth Permissions above).
181
+
182
+ ### Sensor shows as inactive / "No data received"
183
+
184
+ The plugin automatically marks sensors as inactive when no BLE advertisement is received for several minutes. This typically means:
185
+
186
+ - The Aranet4 is out of BLE range (move it closer, typically within 10m)
187
+ - The device is powered off or battery is dead
188
+ - BLE interference from other devices
189
+
190
+ The sensor will automatically recover when advertisements resume -- no restart required.
191
+
192
+ ## Development
193
+
194
+ ```bash
195
+ git clone https://github.com/RobSim/homebridge-aranet4-advanced.git
196
+ cd homebridge-aranet4-advanced
197
+ npm install
198
+ npm run build
199
+ npm test
200
+ ```
201
+
202
+ ### Project structure
203
+
204
+ ```
205
+ src/
206
+ index.ts -- Plugin registration
207
+ platform.ts -- Dynamic platform plugin (lifecycle, accessory management)
208
+ platformAccessory.ts -- HomeKit service/characteristic setup per device
209
+ bleManager.ts -- BLE scanning and advertisement parsing
210
+ aranet4Parser.ts -- Binary protocol parser for Aranet4 data
211
+ settings.ts -- Constants, types, UUIDs
212
+ test/
213
+ *.test.ts -- Jest test suites
214
+ ```
215
+
216
+ ## License
217
+
218
+ MIT
@@ -0,0 +1,88 @@
1
+ {
2
+ "pluginAlias": "Aranet4",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Connect your Aranet4 CO2 sensor to HomeKit with full history support.",
6
+ "footerDisplay": "For help, see the [README](https://github.com/RobSim/homebridge-aranet4-advanced#readme).",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "name": {
11
+ "title": "Platform Name",
12
+ "type": "string",
13
+ "default": "Aranet4",
14
+ "required": true,
15
+ "description": "Display name for this platform instance."
16
+ },
17
+ "devices": {
18
+ "title": "Devices",
19
+ "type": "array",
20
+ "items": {
21
+ "type": "object",
22
+ "properties": {
23
+ "name": {
24
+ "title": "Device Name",
25
+ "type": "string",
26
+ "default": "Aranet4",
27
+ "description": "A friendly name for this sensor."
28
+ },
29
+ "address": {
30
+ "title": "MAC Address",
31
+ "type": "string",
32
+ "description": "Bluetooth MAC address of the Aranet4 device (e.g., AA:BB:CC:DD:EE:FF). Leave blank for auto-discovery.",
33
+ "pattern": "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"
34
+ },
35
+ "pollingInterval": {
36
+ "title": "Polling Interval (seconds)",
37
+ "type": "integer",
38
+ "default": 60,
39
+ "minimum": 60,
40
+ "maximum": 3600,
41
+ "description": "How often to read sensor data, in seconds. Minimum 60s."
42
+ },
43
+ "co2AlertThreshold": {
44
+ "title": "CO2 Alert Threshold (ppm)",
45
+ "type": "integer",
46
+ "default": 1000,
47
+ "minimum": 400,
48
+ "maximum": 5000,
49
+ "description": "CO2 level (ppm) at which CarbonDioxideDetected triggers."
50
+ },
51
+ "lowBatteryThreshold": {
52
+ "title": "Low Battery Threshold (%)",
53
+ "type": "integer",
54
+ "default": 15,
55
+ "minimum": 5,
56
+ "maximum": 50,
57
+ "description": "Battery percentage below which low-battery alert triggers."
58
+ },
59
+ "enableHistory": {
60
+ "title": "Enable Eve History",
61
+ "type": "boolean",
62
+ "default": true,
63
+ "description": "Enable historical graphs in the Eve app."
64
+ }
65
+ },
66
+ "required": ["name"]
67
+ }
68
+ },
69
+ "supabase": {
70
+ "title": "Supabase",
71
+ "type": "object",
72
+ "properties": {
73
+ "url": {
74
+ "title": "Project URL",
75
+ "type": "string",
76
+ "placeholder": "https://yourproject.supabase.co"
77
+ },
78
+ "key": {
79
+ "title": "Secret API Key",
80
+ "type": "string",
81
+ "placeholder": "your-secret-api-key",
82
+ "secret": true
83
+ }
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,57 @@
1
+ import { Aranet4Reading } from './settings';
2
+ /**
3
+ * Validate parsed sensor values. Returns a human-readable reason string
4
+ * if the reading should be rejected, or null if it looks valid.
5
+ */
6
+ export declare function validateReading(reading: {
7
+ co2: number;
8
+ temperature: number;
9
+ pressure: number;
10
+ humidity: number;
11
+ battery: number;
12
+ }): string | null;
13
+ /**
14
+ * Parse a 13-byte extended readings buffer from the Aranet4 BLE characteristic
15
+ * (UUID suffix 3001) into a typed Aranet4Reading.
16
+ *
17
+ * Packet layout (little-endian):
18
+ * Offset 0–1 : uint16 CO2 (ppm, raw)
19
+ * Offset 2–3 : uint16 Temperature (raw ÷ 20 = °C)
20
+ * Offset 4–5 : uint16 Pressure (raw ÷ 10 = hPa)
21
+ * Offset 6 : uint8 Humidity (%, raw)
22
+ * Offset 7 : uint8 Battery (%, raw)
23
+ * Offset 8 : uint8 Status byte
24
+ * Offset 9–10 : uint16 Interval (seconds)
25
+ * Offset 11–12: uint16 Age (seconds since last measurement)
26
+ */
27
+ export declare function parseExtendedReadings(buf: Buffer): Aranet4Reading;
28
+ /**
29
+ * Parse Aranet4 manufacturer-specific advertisement data.
30
+ *
31
+ * When "Smart Home integrations" is enabled in the Aranet4 Home app, the
32
+ * device includes sensor data in its BLE advertisement manufacturer data.
33
+ *
34
+ * The manufacturer data starts with a 2-byte company ID (0x0702 for SAF
35
+ * Tehnika), followed by the payload. Noble includes this company ID prefix
36
+ * in the buffer.
37
+ *
38
+ * Payload layout (little-endian, after 2-byte company ID):
39
+ * Byte 0 : uint8 Flags (bit 5 = integrations enabled)
40
+ * Byte 1–3 : uint8×3 Firmware version (patch, minor, major)
41
+ * Byte 4–5 : uint16 Device type / padding
42
+ * Byte 6–7 : uint16 Additional header
43
+ * Byte 8–9 : uint16 CO2 (ppm)
44
+ * Byte 10–11 : uint16 Temperature (raw ÷ 20 = °C)
45
+ * Byte 12–13 : uint16 Pressure (raw ÷ 10 = hPa)
46
+ * Byte 14 : uint8 Humidity (%)
47
+ * Byte 15 : uint8 Battery (%)
48
+ * Byte 16 : uint8 Status flags
49
+ * Byte 17–18 : uint16 Interval (seconds)
50
+ * Byte 19–20 : uint16 Age (seconds since last measurement)
51
+ *
52
+ * Reference: https://github.com/Anrijs/Aranet4-Python
53
+ * https://github.com/Anrijs/Aranet4-ESP32
54
+ *
55
+ * Returns null if the buffer doesn't contain valid Aranet4 data.
56
+ */
57
+ export declare function parseAdvertisement(manufacturerData: Buffer): Aranet4Reading | null;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateReading = validateReading;
4
+ exports.parseExtendedReadings = parseExtendedReadings;
5
+ exports.parseAdvertisement = parseAdvertisement;
6
+ // ---------------------------------------------------------------------------
7
+ // Plausible sensor value ranges for sanity checking.
8
+ // Values outside these ranges indicate sensor warmup, malfunction, or bad data.
9
+ // ---------------------------------------------------------------------------
10
+ /** CO2 0 or 65535 = sensor warmup / invalid. Valid range: 0–10000 ppm. */
11
+ const CO2_MIN = 1;
12
+ const CO2_MAX = 10_000;
13
+ /** Temperature valid range: −40 °C to +60 °C (Aranet4 spec: 0–50 °C). */
14
+ const TEMP_MIN = -40;
15
+ const TEMP_MAX = 60;
16
+ /** Humidity valid range: 0–100 %. */
17
+ const HUMIDITY_MAX = 100;
18
+ /** Pressure valid range: 300–1200 hPa (covers Dead Sea to Everest). */
19
+ const PRESSURE_MIN = 300;
20
+ const PRESSURE_MAX = 1200;
21
+ /**
22
+ * Validate parsed sensor values. Returns a human-readable reason string
23
+ * if the reading should be rejected, or null if it looks valid.
24
+ */
25
+ function validateReading(reading) {
26
+ if (reading.co2 < CO2_MIN || reading.co2 > CO2_MAX) {
27
+ return `CO2 out of range: ${reading.co2} ppm`;
28
+ }
29
+ if (reading.temperature < TEMP_MIN || reading.temperature > TEMP_MAX) {
30
+ return `Temperature out of range: ${reading.temperature} °C`;
31
+ }
32
+ if (reading.humidity > HUMIDITY_MAX) {
33
+ return `Humidity out of range: ${reading.humidity}%`;
34
+ }
35
+ if (reading.pressure !== 0 && (reading.pressure < PRESSURE_MIN || reading.pressure > PRESSURE_MAX)) {
36
+ return `Pressure out of range: ${reading.pressure} hPa`;
37
+ }
38
+ if (reading.battery > 100) {
39
+ return `Battery out of range: ${reading.battery}%`;
40
+ }
41
+ return null;
42
+ }
43
+ /**
44
+ * Parse a 13-byte extended readings buffer from the Aranet4 BLE characteristic
45
+ * (UUID suffix 3001) into a typed Aranet4Reading.
46
+ *
47
+ * Packet layout (little-endian):
48
+ * Offset 0–1 : uint16 CO2 (ppm, raw)
49
+ * Offset 2–3 : uint16 Temperature (raw ÷ 20 = °C)
50
+ * Offset 4–5 : uint16 Pressure (raw ÷ 10 = hPa)
51
+ * Offset 6 : uint8 Humidity (%, raw)
52
+ * Offset 7 : uint8 Battery (%, raw)
53
+ * Offset 8 : uint8 Status byte
54
+ * Offset 9–10 : uint16 Interval (seconds)
55
+ * Offset 11–12: uint16 Age (seconds since last measurement)
56
+ */
57
+ function parseExtendedReadings(buf) {
58
+ if (buf.length < 13) {
59
+ throw new Error(`Expected at least 13 bytes for extended readings, got ${buf.length}`);
60
+ }
61
+ const co2 = buf.readUInt16LE(0);
62
+ const temperatureRaw = buf.readUInt16LE(2);
63
+ const pressureRaw = buf.readUInt16LE(4);
64
+ const humidity = buf.readUInt8(6);
65
+ const battery = buf.readUInt8(7);
66
+ const status = buf.readUInt8(8);
67
+ const interval = buf.readUInt16LE(9);
68
+ const age = buf.readUInt16LE(11);
69
+ const reading = {
70
+ co2,
71
+ temperature: temperatureRaw / 20,
72
+ pressure: pressureRaw / 10,
73
+ humidity,
74
+ battery,
75
+ status,
76
+ interval,
77
+ age,
78
+ timestamp: Date.now(),
79
+ };
80
+ const invalid = validateReading(reading);
81
+ if (invalid) {
82
+ throw new Error(`Invalid sensor data: ${invalid}`);
83
+ }
84
+ return reading;
85
+ }
86
+ /**
87
+ * Parse Aranet4 manufacturer-specific advertisement data.
88
+ *
89
+ * When "Smart Home integrations" is enabled in the Aranet4 Home app, the
90
+ * device includes sensor data in its BLE advertisement manufacturer data.
91
+ *
92
+ * The manufacturer data starts with a 2-byte company ID (0x0702 for SAF
93
+ * Tehnika), followed by the payload. Noble includes this company ID prefix
94
+ * in the buffer.
95
+ *
96
+ * Payload layout (little-endian, after 2-byte company ID):
97
+ * Byte 0 : uint8 Flags (bit 5 = integrations enabled)
98
+ * Byte 1–3 : uint8×3 Firmware version (patch, minor, major)
99
+ * Byte 4–5 : uint16 Device type / padding
100
+ * Byte 6–7 : uint16 Additional header
101
+ * Byte 8–9 : uint16 CO2 (ppm)
102
+ * Byte 10–11 : uint16 Temperature (raw ÷ 20 = °C)
103
+ * Byte 12–13 : uint16 Pressure (raw ÷ 10 = hPa)
104
+ * Byte 14 : uint8 Humidity (%)
105
+ * Byte 15 : uint8 Battery (%)
106
+ * Byte 16 : uint8 Status flags
107
+ * Byte 17–18 : uint16 Interval (seconds)
108
+ * Byte 19–20 : uint16 Age (seconds since last measurement)
109
+ *
110
+ * Reference: https://github.com/Anrijs/Aranet4-Python
111
+ * https://github.com/Anrijs/Aranet4-ESP32
112
+ *
113
+ * Returns null if the buffer doesn't contain valid Aranet4 data.
114
+ */
115
+ function parseAdvertisement(manufacturerData) {
116
+ // Company ID (2) + header (8) + CO2 (2) + temperature (2) = 14 bytes minimum
117
+ if (manufacturerData.length < 14) {
118
+ return null;
119
+ }
120
+ // Skip 2-byte company ID — remaining bytes are the payload
121
+ const d = manufacturerData.subarray(2);
122
+ // Need at least 12 bytes: 8-byte header + CO2 (2) + temperature (2)
123
+ if (d.length < 12) {
124
+ return null;
125
+ }
126
+ // Sensor data starts at byte 8, after the 8-byte header
127
+ const co2 = d.readUInt16LE(8);
128
+ const temperatureRaw = d.readUInt16LE(10);
129
+ // Remaining fields may not be present in shorter advertisements
130
+ const pressure = d.length >= 14 ? d.readUInt16LE(12) / 10 : 0;
131
+ const humidity = d.length >= 15 ? d.readUInt8(14) : 0;
132
+ const battery = d.length >= 16 ? d.readUInt8(15) : 0;
133
+ const status = d.length >= 17 ? d.readUInt8(16) : 0;
134
+ const interval = d.length >= 19 ? d.readUInt16LE(17) : 0;
135
+ const age = d.length >= 21 ? d.readUInt16LE(19) : 0;
136
+ const reading = {
137
+ co2,
138
+ temperature: temperatureRaw / 20,
139
+ pressure,
140
+ humidity,
141
+ battery,
142
+ status,
143
+ interval,
144
+ age,
145
+ timestamp: Date.now(),
146
+ };
147
+ const invalid = validateReading(reading);
148
+ if (invalid) {
149
+ return null; // Silently skip — advertisements can contain stale/warmup data
150
+ }
151
+ return reading;
152
+ }
153
+ //# sourceMappingURL=aranet4Parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aranet4Parser.js","sourceRoot":"","sources":["../src/aranet4Parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import { Logger } from 'homebridge';
2
+ import { Aranet4Reading, Aranet4DeviceConfig } from './settings';
3
+ /** Callback invoked each time fresh sensor data arrives. */
4
+ export type ReadingCallback = (deviceId: string, reading: Aranet4Reading) => void;
5
+ /** Callback invoked when a device hasn't been heard from in too long. */
6
+ export type StaleCallback = (deviceId: string) => void;
7
+ export declare class BleManager {
8
+ private readonly log;
9
+ private readonly deviceConfigs;
10
+ private readonly devices;
11
+ private scanning;
12
+ private readingCallback;
13
+ private staleCallback;
14
+ private poweredOn;
15
+ private shuttingDown;
16
+ private scanRestartTimer;
17
+ private scanRetryTimer;
18
+ private staleCheckTimer;
19
+ private scanRetryDelay;
20
+ private loggedUnauthorized;
21
+ private readonly onStateChange;
22
+ private readonly onDiscover;
23
+ private readonly onScanStop;
24
+ private readonly onWarning;
25
+ constructor(log: Logger, deviceConfigs: Aranet4DeviceConfig[]);
26
+ /** Register a callback that will be invoked on every new reading. */
27
+ onReading(cb: ReadingCallback): void;
28
+ /** Register a callback for when a device goes stale (no data for too long). */
29
+ onStale(cb: StaleCallback): void;
30
+ /** Begin scanning. Call once after Homebridge finishes launching. */
31
+ start(): void;
32
+ /** Gracefully shut down — stop scanning, clear all timers. */
33
+ shutdown(): void;
34
+ private handleStateChange;
35
+ private startScan;
36
+ private stopScan;
37
+ /**
38
+ * Handle noble's scanStop event — scanning was interrupted externally
39
+ * (another app took BLE, adapter reset, etc.).
40
+ *
41
+ * Debounced: noble issue #569 documents scanStop firing rapidly after
42
+ * reboots. We only schedule one restart attempt at a time.
43
+ */
44
+ private handleScanStop;
45
+ /**
46
+ * Schedule a scan retry with exponential backoff after a startScanning failure.
47
+ */
48
+ private scheduleScanRetry;
49
+ private checkForStaleDevices;
50
+ private clearTimers;
51
+ private handleDiscovery;
52
+ private processAdvertisement;
53
+ private handleAranet4Advertisement;
54
+ private findConfigForPeripheral;
55
+ private buildDefaultConfig;
56
+ }