homebridge-pollen 1.0.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.
@@ -0,0 +1 @@
1
+ npx lint-staged
package/CLAUDE.md ADDED
@@ -0,0 +1,98 @@
1
+ # CLAUDE.md
2
+
3
+ ## Project Overview
4
+
5
+ Homebridge dynamic platform plugin that fetches pollen data from the Ambee API and exposes it as HomeKit accessories. No runtime dependencies — uses Node.js 20+ built-in `fetch`.
6
+
7
+ ## Commands
8
+
9
+ - `npm run build` — clean compile TypeScript to `dist/`
10
+ - `npm run lint` — ESLint
11
+ - `npm run dev` — build, link, and start Homebridge with nodemon (auto-restarts on src changes)
12
+
13
+ ## Homebridge Plugin Architecture
14
+
15
+ ### Registration
16
+
17
+ A Homebridge plugin exports a default function that receives the `API` object and calls `api.registerPlatform(PLATFORM_NAME, PlatformClass)`. The platform name here must match `pluginAlias` in `config.schema.json`. The npm package name (`homebridge-pollen`) is how Homebridge discovers the plugin.
18
+
19
+ ### Dynamic Platform Plugin Lifecycle
20
+
21
+ The plugin implements `DynamicPlatformPlugin` from `homebridge`. The lifecycle is:
22
+
23
+ 1. **Constructor** — called with `(log, config, api)`. Set up state but do NOT create accessories here. Register a `didFinishLaunching` listener on the API.
24
+ 2. **`configureAccessory(accessory)`** — Homebridge calls this for each accessory it has cached on disk. The plugin must store these in a list. This is called *before* `didFinishLaunching`.
25
+ 3. **`didFinishLaunching` event** — now safe to create/remove accessories. Compare desired accessories against the cached list:
26
+ - Cached and still desired → restore (call `api.updatePlatformAccessories`)
27
+ - Not cached → create via `new api.platformAccessory(name, uuid)` then `api.registerPlatformAccessories`
28
+ - Cached but no longer desired → remove via `api.unregisterPlatformAccessories`
29
+ 4. **`shutdown` event** — clean up timers and connections.
30
+
31
+ ### Accessory UUIDs
32
+
33
+ UUIDs must be deterministic and stable across restarts. Generate them with `api.hap.uuid.generate(stableString)`. This plugin uses `pollen-{category}-{level}-{location}` as the stable string. If the UUID changes, Homebridge treats it as a new accessory and the old one becomes stale.
34
+
35
+ ### Services and Characteristics
36
+
37
+ Each `PlatformAccessory` has one or more `Service` objects. Always check `accessory.getService(ServiceType)` before calling `addService` to avoid duplicates on restore from cache.
38
+
39
+ Key pattern for getting/setting services:
40
+ ```ts
41
+ const service = accessory.getService(api.hap.Service.ContactSensor)
42
+ || accessory.addService(api.hap.Service.ContactSensor, displayName);
43
+ ```
44
+
45
+ **Push model** — use `service.updateCharacteristic(CharType, value)` to push updates. This is preferred over registering `onGet` handlers when data is fetched on a polling interval. HomeKit GET requests return the last pushed value instantly.
46
+
47
+ ### HomeKit Service Types Used
48
+
49
+ - **ContactSensor** — `ContactSensorState`: `CONTACT_DETECTED` (0) or `CONTACT_NOT_DETECTED` (1). Used because contact sensor state changes are native HomeKit automation triggers ("When X detected...").
50
+ - **AirQualitySensor** — `AirQuality`: `UNKNOWN` (0), `EXCELLENT` (1), `GOOD` (2), `FAIR` (3), `INFERIOR` (4), `POOR` (5).
51
+ - **AccessoryInformation** — always present on every accessory. Set `Manufacturer`, `Model`, `SerialNumber`.
52
+
53
+ ### config.schema.json
54
+
55
+ This file defines the Homebridge UI configuration form. Key fields:
56
+
57
+ - `pluginAlias` — must match `PLATFORM_NAME` passed to `api.registerPlatform()`
58
+ - `pluginType` — `"platform"` for platform plugins
59
+ - `singular: true` — only one instance of this platform allowed
60
+ - `schema.properties` — defines form fields. Use `"required": true` on required fields. Nested objects with `"expandable": true` render as collapsible sections in the UI.
61
+
62
+ ### TypeScript Configuration
63
+
64
+ - Target `ES2022` with `module: nodenext` / `moduleResolution: nodenext`
65
+ - Imports must use `.js` extensions (e.g., `import { Foo } from './foo.js'`) — this is required by nodenext module resolution even though the source files are `.ts`
66
+ - `@types/node` is needed for `fetch`, `setInterval`, `clearInterval` globals
67
+
68
+ ## Ambee API
69
+
70
+ - **Endpoint**: `GET https://api.ambeedata.com/latest/pollen/by-place?place={location}`
71
+ - **Auth**: `x-api-key` header with API key
72
+ - **Free tier**: 100 calls/day — default 60-min polling = 24 calls/day
73
+ - **Response shape**: `{ message: string, data: [{ Count: { grass_pollen, tree_pollen, weed_pollen }, Risk: {...}, Species: {...}, updatedAt: string }] }`
74
+ - The `by-place` endpoint accepts zip codes and place names directly
75
+
76
+ ## Pollen Thresholds
77
+
78
+ Default thresholds based on NAB (National Allergy Bureau) guidelines. A count is classified as:
79
+ - **Low** if `count <= threshold.low`
80
+ - **High** if `count >= threshold.high`
81
+ - **Medium** otherwise
82
+
83
+ | Category | Low | High |
84
+ |----------|-----|------|
85
+ | Overall | 50 | 200 |
86
+ | Tree | 15 | 90 |
87
+ | Grass | 20 | 200 |
88
+ | Weed | 10 | 50 |
89
+
90
+ Users can override these via `config.thresholds`.
91
+
92
+ ## Error Handling
93
+
94
+ - Missing `apiKey`/`location` — logs error, plugin does not start polling
95
+ - 401 — logs clear message about invalid API key, returns cached data
96
+ - 429 — logs warning, increments consecutive failure counter for exponential backoff
97
+ - Network/5xx — logs warning, returns cached data, retries next cycle
98
+ - Empty data array — logs warning about unsupported location, retains cache
@@ -0,0 +1,89 @@
1
+ # Contributing
2
+
3
+ ## Local Development
4
+
5
+ ### Prerequisites
6
+
7
+ - Node.js 20 or later
8
+ - An [Ambee API key](https://api-dashboard.getambee.com/) (free tier works fine)
9
+
10
+ ### Quick start
11
+
12
+ Run the setup script to install dependencies, create the local Homebridge config, and link the plugin:
13
+
14
+ ```sh
15
+ npm run setup
16
+ ```
17
+
18
+ The script will prompt you for your Ambee API key and location.
19
+
20
+ ### Development
21
+
22
+ ```sh
23
+ npm run dev
24
+ ```
25
+
26
+ The development workflow runs an isolated local Homebridge instance, completely separate from any system-wide Homebridge installation you may have:
27
+
28
+ - `./.homebridge` folder holds its own `config.json` and cache
29
+ - **`npm link`** — Makes the plugin discoverable to Homebridge without publishing to npm
30
+ - **`nodemon`** — Watches `src/` and automatically rebuilds TypeScript and restarts Homebridge on every change
31
+
32
+
33
+ ### Build
34
+
35
+ ```sh
36
+ npm run build
37
+ ```
38
+
39
+ ### Testing the plugin
40
+
41
+ After running `npm run setup` and `npm run dev`, verify in the console output that:
42
+
43
+ - 3 ContactSensor accessories are registered (Pollen High, Pollen Medium, Pollen Low)
44
+ - An initial pollen fetch completes and one sensor shows `DETECTED`
45
+ - Debug log lines show the fetched counts and level classifications
46
+
47
+ To test optional features, edit `.homebridge/config.json` and add:
48
+
49
+ ```json
50
+ {
51
+ "platform": "HomebridgePollen",
52
+ "apiKey": "YOUR_AMBEE_API_KEY",
53
+ "location": "10001",
54
+ "enableCategorySensors": true,
55
+ "enableAirQualitySensor": true
56
+ }
57
+ ```
58
+
59
+ This adds 9 per-category sensors (Tree/Grass/Weed x High/Medium/Low) and 1 AirQualitySensor. Restart the dev server to pick up the changes.
60
+
61
+ ### Pairing with Apple Home
62
+
63
+ To test end-to-end in the Home app:
64
+
65
+ 1. Open the Home app on iOS or macOS
66
+ 2. Tap **+** > **Add Accessory** > **More Options...**
67
+ 3. Select "Test Homebridge" from the list
68
+ 4. Enter the PIN: `031-45-154`
69
+
70
+ The test bridge uses port 51826, so it can run alongside a production Homebridge instance on the default port. When you're done testing, unpair the bridge from Home to avoid stale accessories.
71
+
72
+ ### Project Structure
73
+
74
+ ```
75
+ src/
76
+ ├── index.ts # Entry point — registers platform with Homebridge
77
+ ├── settings.ts # Constants and default thresholds
78
+ ├── types.ts # TypeScript interfaces and helper functions
79
+ ├── pollenService.ts # Ambee API client (fetch, parse, cache)
80
+ ├── platform.ts # DynamicPlatformPlugin (discovery, polling)
81
+ └── platformAccessory.ts # Maps pollen data to HomeKit services
82
+ ```
83
+
84
+ ## Submitting Changes
85
+
86
+ 1. Create a branch from `main`.
87
+ 2. Make your changes and verify `npm run build` and `npm run lint` pass.
88
+ 3. Test against a local Homebridge instance using the steps above.
89
+ 4. Open a pull request with a description of what changed and why.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Patrick Burtchaell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # homebridge-pollen
2
+
3
+ [![npm](https://img.shields.io/npm/v/homebridge-pollen)](https://www.npmjs.com/package/homebridge-pollen)
4
+ [![license](https://img.shields.io/npm/l/homebridge-pollen)](LICENSE)
5
+
6
+ Homebridge plugin that exposes pollen levels as HomeKit sensors using the [Ambee API](https://www.getambee.com/).
7
+
8
+ ## Features
9
+
10
+ - **Contact sensors for automations** — Pollen levels are exposed as contact sensors, which can trigger HomeKit automations natively ("When Pollen High detected...")
11
+ - **Per-category sensors** — Optionally add separate sensors for tree, grass, and weed pollen
12
+ - **Air quality sensor** — Optionally add an air quality sensor that maps overall pollen count to a 1-5 severity scale
13
+ - **No runtime dependencies** — Uses Node.js 20+ built-in `fetch`
14
+
15
+ ## Requirements
16
+
17
+ - Node.js 20.0.0 or later
18
+ - Homebridge 1.8.0 or later (including Homebridge 2.x)
19
+
20
+ ## Installation
21
+
22
+ ### Using the Homebridge UI
23
+
24
+ Search for `homebridge-pollen` in the Homebridge UI plugin search and install it.
25
+
26
+ ### Using npm
27
+
28
+ ```bash
29
+ npm install -g homebridge-pollen
30
+ ```
31
+
32
+ ## Getting an API key
33
+
34
+ This plugin requires an API key from Ambee to fetch pollen data.
35
+
36
+ 1. Go to the [Ambee API Dashboard](https://api-dashboard.getambee.com/)
37
+ 2. Create a free account
38
+ 3. Copy your API key from the dashboard
39
+
40
+ The free tier allows 100 API calls per day. With the default 60-minute poll interval, the plugin uses approximately 24 calls per day.
41
+
42
+ ## Configuration
43
+
44
+ You can configure the plugin using the Homebridge UI or by editing your `config.json` directly.
45
+
46
+ ### Example configuration
47
+
48
+ ```json
49
+ {
50
+ "platforms": [
51
+ {
52
+ "platform": "HomebridgePollen",
53
+ "apiKey": "your-ambee-api-key",
54
+ "location": "10001"
55
+ }
56
+ ]
57
+ }
58
+ ```
59
+
60
+ ### Configuration options
61
+
62
+ | Option | Required | Default | Description |
63
+ |--------|----------|---------|-------------|
64
+ | `platform` | Yes | — | Must be `HomebridgePollen` |
65
+ | `apiKey` | Yes | — | Your Ambee API key |
66
+ | `location` | Yes | — | Zip code or place name to fetch pollen data for |
67
+ | `pollInterval` | No | `60` | How often to fetch pollen data, in minutes (minimum 15) |
68
+ | `enableCategorySensors` | No | `false` | Add 9 additional sensors for tree/grass/weed at each level |
69
+ | `enableAirQualitySensor` | No | `false` | Add an air quality sensor accessory |
70
+ | `thresholds` | No | — | Override default pollen count thresholds (see below) |
71
+
72
+ ## How it works
73
+
74
+ The plugin creates contact sensors that represent pollen levels. By default, you get three sensors:
75
+
76
+ - **Pollen High** — Triggered when pollen count is high
77
+ - **Pollen Medium** — Triggered when pollen count is medium
78
+ - **Pollen Low** — Triggered when pollen count is low
79
+
80
+ Contact sensors are used because their state changes are native HomeKit automation triggers. This lets you create automations like "When Pollen High is detected, turn on the air purifier."
81
+
82
+ ### Optional sensors
83
+
84
+ When `enableCategorySensors` is enabled, you get 9 additional sensors:
85
+
86
+ - Tree Pollen High / Medium / Low
87
+ - Grass Pollen High / Medium / Low
88
+ - Weed Pollen High / Medium / Low
89
+
90
+ When `enableAirQualitySensor` is enabled, you get a single air quality sensor that maps the overall pollen count to HomeKit's 5-level air quality scale.
91
+
92
+ ## Pollen thresholds
93
+
94
+ Pollen counts are classified as Low, Medium, or High based on thresholds from the National Allergy Bureau (NAB). You can override these defaults in your configuration.
95
+
96
+ | Category | Low (at or below) | High (at or above) |
97
+ |----------|-------------------|---------------------|
98
+ | Overall | 50 | 200 |
99
+ | Tree | 15 | 90 |
100
+ | Grass | 20 | 200 |
101
+ | Weed | 10 | 50 |
102
+
103
+ Counts between the low and high thresholds are classified as Medium.
104
+
105
+ ### Customizing thresholds
106
+
107
+ ```json
108
+ {
109
+ "platforms": [
110
+ {
111
+ "platform": "HomebridgePollen",
112
+ "apiKey": "your-api-key",
113
+ "location": "10001",
114
+ "thresholds": {
115
+ "overall": { "low": 30, "high": 150 },
116
+ "tree": { "low": 10, "high": 60 }
117
+ }
118
+ }
119
+ ]
120
+ }
121
+ ```
122
+
123
+ ## License
124
+
125
+ [MIT](LICENSE)
@@ -0,0 +1,118 @@
1
+ {
2
+ "pluginAlias": "HomebridgePollen",
3
+ "pluginType": "platform",
4
+ "singular": true,
5
+ "headerDisplay": "Exposes pollen levels from the [Ambee API](https://www.getambee.com/) as HomeKit contact sensors for automation.",
6
+ "footerDisplay": "For help, see the [README](https://github.com/pburtchaell/homebridge-pollen).",
7
+ "schema": {
8
+ "type": "object",
9
+ "properties": {
10
+ "apiKey": {
11
+ "title": "API Key",
12
+ "type": "string",
13
+ "required": true,
14
+ "description": "Your Ambee API key. Get one at https://api-dashboard.getambee.com/"
15
+ },
16
+ "location": {
17
+ "title": "Location",
18
+ "type": "string",
19
+ "required": true,
20
+ "placeholder": "10001",
21
+ "description": "Zip code or place name to fetch pollen data for."
22
+ },
23
+ "pollInterval": {
24
+ "title": "Poll Interval (minutes)",
25
+ "type": "integer",
26
+ "default": 60,
27
+ "minimum": 15,
28
+ "description": "How often to fetch pollen data from Ambee. Minimum 15 minutes. Free tier allows ~100 calls/day."
29
+ },
30
+ "enableCategorySensors": {
31
+ "title": "Enable Per-Category Sensors",
32
+ "type": "boolean",
33
+ "default": false,
34
+ "description": "Add 9 additional sensors for Tree/Grass/Weed x High/Medium/Low."
35
+ },
36
+ "enableAirQualitySensor": {
37
+ "title": "Enable Air Quality Sensor",
38
+ "type": "boolean",
39
+ "default": false,
40
+ "description": "Add an AirQualitySensor accessory that maps overall pollen count to a 1-5 severity scale."
41
+ },
42
+ "thresholds": {
43
+ "title": "Threshold Overrides",
44
+ "type": "object",
45
+ "expandable": true,
46
+ "description": "Override default pollen count thresholds for level classification.",
47
+ "properties": {
48
+ "overall": {
49
+ "title": "Overall",
50
+ "type": "object",
51
+ "properties": {
52
+ "low": {
53
+ "title": "Low Threshold",
54
+ "type": "integer",
55
+ "default": 50,
56
+ "description": "Counts at or below this value are classified as Low."
57
+ },
58
+ "high": {
59
+ "title": "High Threshold",
60
+ "type": "integer",
61
+ "default": 200,
62
+ "description": "Counts at or above this value are classified as High."
63
+ }
64
+ }
65
+ },
66
+ "tree": {
67
+ "title": "Tree",
68
+ "type": "object",
69
+ "properties": {
70
+ "low": {
71
+ "title": "Low Threshold",
72
+ "type": "integer",
73
+ "default": 15
74
+ },
75
+ "high": {
76
+ "title": "High Threshold",
77
+ "type": "integer",
78
+ "default": 90
79
+ }
80
+ }
81
+ },
82
+ "grass": {
83
+ "title": "Grass",
84
+ "type": "object",
85
+ "properties": {
86
+ "low": {
87
+ "title": "Low Threshold",
88
+ "type": "integer",
89
+ "default": 20
90
+ },
91
+ "high": {
92
+ "title": "High Threshold",
93
+ "type": "integer",
94
+ "default": 200
95
+ }
96
+ }
97
+ },
98
+ "weed": {
99
+ "title": "Weed",
100
+ "type": "object",
101
+ "properties": {
102
+ "low": {
103
+ "title": "Low Threshold",
104
+ "type": "integer",
105
+ "default": 10
106
+ },
107
+ "high": {
108
+ "title": "High Threshold",
109
+ "type": "integer",
110
+ "default": 50
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
@@ -0,0 +1,3 @@
1
+ import type { API } from 'homebridge';
2
+ declare const _default: (api: API) => void;
3
+ export default _default;
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const settings_js_1 = require("./settings.js");
4
+ const platform_js_1 = require("./platform.js");
5
+ exports.default = (api) => {
6
+ api.registerPlatform(settings_js_1.PLATFORM_NAME, platform_js_1.PollenPlatform);
7
+ };
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AACA,+CAA8C;AAC9C,+CAA+C;AAE/C,kBAAe,CAAC,GAAQ,EAAE,EAAE;IAC1B,GAAG,CAAC,gBAAgB,CAAC,2BAAa,EAAE,4BAAc,CAAC,CAAC;AACtD,CAAC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig } from 'homebridge';
2
+ export declare class PollenPlatform implements DynamicPlatformPlugin {
3
+ readonly log: Logger;
4
+ readonly config: PlatformConfig;
5
+ readonly api: API;
6
+ private readonly cachedAccessories;
7
+ private readonly handlers;
8
+ private pollenService?;
9
+ private pollTimer?;
10
+ constructor(log: Logger, config: PlatformConfig, api: API);
11
+ configureAccessory(accessory: PlatformAccessory): void;
12
+ private discoverDevices;
13
+ private buildAccessoryDefinitions;
14
+ private startPolling;
15
+ private poll;
16
+ }
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PollenPlatform = void 0;
4
+ const settings_js_1 = require("./settings.js");
5
+ const types_js_1 = require("./types.js");
6
+ const pollenService_js_1 = require("./pollenService.js");
7
+ const platformAccessory_js_1 = require("./platformAccessory.js");
8
+ class PollenPlatform {
9
+ log;
10
+ config;
11
+ api;
12
+ cachedAccessories = [];
13
+ handlers = new Map();
14
+ pollenService;
15
+ pollTimer;
16
+ constructor(log, config, api) {
17
+ this.log = log;
18
+ this.config = config;
19
+ this.api = api;
20
+ const pollenConfig = config;
21
+ if (!pollenConfig.apiKey || !pollenConfig.location) {
22
+ this.log.error('Missing required configuration: apiKey and location must be set. '
23
+ + 'Plugin will not start.');
24
+ return;
25
+ }
26
+ const thresholds = (0, types_js_1.resolveThresholds)(pollenConfig, settings_js_1.DEFAULT_THRESHOLDS);
27
+ this.pollenService = new pollenService_js_1.PollenService(pollenConfig.apiKey, pollenConfig.location, thresholds, this.log);
28
+ this.api.on('didFinishLaunching', () => {
29
+ this.discoverDevices(pollenConfig);
30
+ this.startPolling(pollenConfig);
31
+ });
32
+ this.api.on('shutdown', () => {
33
+ if (this.pollTimer) {
34
+ clearInterval(this.pollTimer);
35
+ }
36
+ });
37
+ }
38
+ configureAccessory(accessory) {
39
+ this.log.info(`Loading accessory from cache: ${accessory.displayName}`);
40
+ this.cachedAccessories.push(accessory);
41
+ }
42
+ discoverDevices(config) {
43
+ const definitions = this.buildAccessoryDefinitions(config);
44
+ const desiredUUIDs = new Set();
45
+ for (const definition of definitions) {
46
+ const uuid = this.api.hap.uuid.generate(definition.id);
47
+ desiredUUIDs.add(uuid);
48
+ const existingAccessory = this.cachedAccessories.find(a => a.UUID === uuid);
49
+ if (existingAccessory) {
50
+ this.log.info(`Restoring accessory from cache: ${existingAccessory.displayName}`);
51
+ existingAccessory.context.definition = definition;
52
+ const handler = new platformAccessory_js_1.PollenAccessoryHandler(existingAccessory, definition, this.api, this.log);
53
+ this.handlers.set(uuid, handler);
54
+ this.api.updatePlatformAccessories([existingAccessory]);
55
+ }
56
+ else {
57
+ this.log.info(`Adding new accessory: ${definition.name}`);
58
+ const accessory = new this.api.platformAccessory(definition.name, uuid);
59
+ accessory.context.definition = definition;
60
+ const handler = new platformAccessory_js_1.PollenAccessoryHandler(accessory, definition, this.api, this.log);
61
+ this.handlers.set(uuid, handler);
62
+ this.api.registerPlatformAccessories(settings_js_1.PLUGIN_NAME, settings_js_1.PLATFORM_NAME, [accessory]);
63
+ }
64
+ }
65
+ // Remove stale accessories
66
+ const staleAccessories = this.cachedAccessories.filter(a => !desiredUUIDs.has(a.UUID));
67
+ if (staleAccessories.length > 0) {
68
+ this.log.info(`Removing ${staleAccessories.length} stale accessory(ies)`);
69
+ this.api.unregisterPlatformAccessories(settings_js_1.PLUGIN_NAME, settings_js_1.PLATFORM_NAME, staleAccessories);
70
+ }
71
+ }
72
+ buildAccessoryDefinitions(config) {
73
+ const location = config.location;
74
+ const definitions = [];
75
+ // Default 3 overall sensors: High, Medium, Low
76
+ const overallLevels = ['High', 'Medium', 'Low'];
77
+ for (const level of overallLevels) {
78
+ definitions.push({
79
+ id: `pollen-overall-${level}-${location}`,
80
+ name: `Pollen ${level}`,
81
+ type: 'contact',
82
+ level,
83
+ category: 'overall',
84
+ });
85
+ }
86
+ // Optional per-category sensors (9 total)
87
+ if (config.enableCategorySensors) {
88
+ const categories = ['tree', 'grass', 'weed'];
89
+ const levels = ['High', 'Medium', 'Low'];
90
+ for (const category of categories) {
91
+ for (const level of levels) {
92
+ const label = category.charAt(0).toUpperCase() + category.slice(1);
93
+ definitions.push({
94
+ id: `pollen-${category}-${level}-${location}`,
95
+ name: `${label} Pollen ${level}`,
96
+ type: 'contact',
97
+ level,
98
+ category,
99
+ });
100
+ }
101
+ }
102
+ }
103
+ // Optional air quality sensor
104
+ if (config.enableAirQualitySensor) {
105
+ definitions.push({
106
+ id: `pollen-airquality-${location}`,
107
+ name: 'Pollen Air Quality',
108
+ type: 'airquality',
109
+ });
110
+ }
111
+ return definitions;
112
+ }
113
+ startPolling(config) {
114
+ if (!this.pollenService) {
115
+ return;
116
+ }
117
+ const intervalMinutes = Math.max(config.pollInterval ?? settings_js_1.DEFAULT_POLL_INTERVAL, settings_js_1.MIN_POLL_INTERVAL);
118
+ const intervalMs = intervalMinutes * 60 * 1000;
119
+ this.log.info(`Polling Ambee API every ${intervalMinutes} minutes for location "${config.location}"`);
120
+ // Immediate first fetch
121
+ this.poll();
122
+ this.pollTimer = setInterval(() => {
123
+ const backoff = this.pollenService.getBackoffMultiplier();
124
+ if (backoff > 1) {
125
+ this.log.debug(`Backoff active (${backoff}x). Skipping this poll cycle.`);
126
+ return;
127
+ }
128
+ this.poll();
129
+ }, intervalMs);
130
+ }
131
+ async poll() {
132
+ if (!this.pollenService) {
133
+ return;
134
+ }
135
+ const data = await this.pollenService.fetchPollenData();
136
+ if (!data) {
137
+ this.log.debug('No pollen data available (no cached data yet).');
138
+ return;
139
+ }
140
+ for (const handler of this.handlers.values()) {
141
+ handler.updatePollenData(data);
142
+ }
143
+ }
144
+ }
145
+ exports.PollenPlatform = PollenPlatform;
146
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { API, Logger, PlatformAccessory } from 'homebridge';
2
+ import type { AccessoryDefinition, ParsedPollenData } from './types.js';
3
+ export declare class PollenAccessoryHandler {
4
+ private readonly accessory;
5
+ private readonly definition;
6
+ private readonly api;
7
+ private readonly log;
8
+ private readonly service;
9
+ private readonly Characteristic;
10
+ constructor(accessory: PlatformAccessory, definition: AccessoryDefinition, api: API, log: Logger);
11
+ updatePollenData(data: ParsedPollenData): void;
12
+ private updateContactSensor;
13
+ private updateAirQuality;
14
+ private mapCountToAirQuality;
15
+ }
@@ -0,0 +1,75 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PollenAccessoryHandler = void 0;
4
+ class PollenAccessoryHandler {
5
+ accessory;
6
+ definition;
7
+ api;
8
+ log;
9
+ service;
10
+ Characteristic;
11
+ constructor(accessory, definition, api, log) {
12
+ this.accessory = accessory;
13
+ this.definition = definition;
14
+ this.api = api;
15
+ this.log = log;
16
+ this.Characteristic = this.api.hap.Characteristic;
17
+ this.accessory.getService(this.api.hap.Service.AccessoryInformation)
18
+ .setCharacteristic(this.Characteristic.Manufacturer, 'Ambee')
19
+ .setCharacteristic(this.Characteristic.Model, 'Pollen Sensor')
20
+ .setCharacteristic(this.Characteristic.SerialNumber, definition.id);
21
+ if (definition.type === 'airquality') {
22
+ this.service = this.accessory.getService(this.api.hap.Service.AirQualitySensor)
23
+ || this.accessory.addService(this.api.hap.Service.AirQualitySensor, definition.name);
24
+ }
25
+ else {
26
+ this.service = this.accessory.getService(this.api.hap.Service.ContactSensor)
27
+ || this.accessory.addService(this.api.hap.Service.ContactSensor, definition.name);
28
+ }
29
+ this.service.setCharacteristic(this.Characteristic.Name, definition.name);
30
+ }
31
+ updatePollenData(data) {
32
+ if (this.definition.type === 'airquality') {
33
+ this.updateAirQuality(data);
34
+ }
35
+ else {
36
+ this.updateContactSensor(data);
37
+ }
38
+ }
39
+ updateContactSensor(data) {
40
+ const category = this.definition.category;
41
+ const targetLevel = this.definition.level;
42
+ const currentLevel = data[category].level;
43
+ const isDetected = currentLevel === targetLevel;
44
+ const state = isDetected
45
+ ? this.Characteristic.ContactSensorState.CONTACT_DETECTED
46
+ : this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED;
47
+ this.service.updateCharacteristic(this.Characteristic.ContactSensorState, state);
48
+ this.log.debug(`${this.definition.name}: ${category} level is ${currentLevel}, `
49
+ + `sensor ${targetLevel} → ${isDetected ? 'DETECTED' : 'NOT_DETECTED'}`);
50
+ }
51
+ updateAirQuality(data) {
52
+ const quality = this.mapCountToAirQuality(data.overall.count);
53
+ this.service.updateCharacteristic(this.Characteristic.AirQuality, quality);
54
+ this.log.debug(`${this.definition.name}: overall count=${data.overall.count}, AirQuality=${quality}`);
55
+ }
56
+ mapCountToAirQuality(count) {
57
+ // Map total pollen count to HomeKit AirQuality enum (0-5)
58
+ // 0=UNKNOWN, 1=EXCELLENT, 2=GOOD, 3=FAIR, 4=INFERIOR, 5=POOR
59
+ if (count <= 20) {
60
+ return this.Characteristic.AirQuality.EXCELLENT;
61
+ }
62
+ if (count <= 80) {
63
+ return this.Characteristic.AirQuality.GOOD;
64
+ }
65
+ if (count <= 200) {
66
+ return this.Characteristic.AirQuality.FAIR;
67
+ }
68
+ if (count <= 400) {
69
+ return this.Characteristic.AirQuality.INFERIOR;
70
+ }
71
+ return this.Characteristic.AirQuality.POOR;
72
+ }
73
+ }
74
+ exports.PollenAccessoryHandler = PollenAccessoryHandler;
75
+ //# sourceMappingURL=platformAccessory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platformAccessory.js","sourceRoot":"","sources":["../src/platformAccessory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { Logger } from 'homebridge';
2
+ import type { Thresholds } from './settings.js';
3
+ import type { ParsedPollenData } from './types.js';
4
+ export declare class PollenService {
5
+ private readonly apiKey;
6
+ private readonly location;
7
+ private readonly thresholds;
8
+ private readonly log;
9
+ private cache;
10
+ private consecutiveFailures;
11
+ constructor(apiKey: string, location: string, thresholds: Thresholds, log: Logger);
12
+ fetchPollenData(): Promise<ParsedPollenData | null>;
13
+ getBackoffMultiplier(): number;
14
+ getCachedData(): ParsedPollenData | null;
15
+ }
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PollenService = void 0;
4
+ const settings_js_1 = require("./settings.js");
5
+ const types_js_1 = require("./types.js");
6
+ class PollenService {
7
+ apiKey;
8
+ location;
9
+ thresholds;
10
+ log;
11
+ cache = null;
12
+ consecutiveFailures = 0;
13
+ constructor(apiKey, location, thresholds, log) {
14
+ this.apiKey = apiKey;
15
+ this.location = location;
16
+ this.thresholds = thresholds;
17
+ this.log = log;
18
+ }
19
+ async fetchPollenData() {
20
+ const url = `${settings_js_1.AMBEE_BASE_URL}/latest/pollen/by-place?place=${encodeURIComponent(this.location)}`;
21
+ try {
22
+ const response = await fetch(url, {
23
+ headers: {
24
+ 'x-api-key': this.apiKey,
25
+ 'Content-Type': 'application/json',
26
+ },
27
+ });
28
+ if (response.status === 401) {
29
+ this.log.error('Ambee API returned 401 Unauthorized. Check that your API key is valid.');
30
+ return this.cache;
31
+ }
32
+ if (response.status === 429) {
33
+ this.consecutiveFailures++;
34
+ this.log.warn('Ambee API rate limit reached (429). Will retry next cycle. '
35
+ + `Consecutive failures: ${this.consecutiveFailures}`);
36
+ return this.cache;
37
+ }
38
+ if (!response.ok) {
39
+ this.consecutiveFailures++;
40
+ this.log.warn(`Ambee API returned ${response.status}. `
41
+ + `Returning cached data. Consecutive failures: ${this.consecutiveFailures}`);
42
+ return this.cache;
43
+ }
44
+ const body = await response.json();
45
+ if (!body.data || body.data.length === 0) {
46
+ this.log.warn(`Ambee API returned no pollen data for location "${this.location}". `
47
+ + 'The location may not be supported. Retaining cached data.');
48
+ return this.cache;
49
+ }
50
+ const item = body.data[0];
51
+ const treeCount = item.Count.tree_pollen;
52
+ const grassCount = item.Count.grass_pollen;
53
+ const weedCount = item.Count.weed_pollen;
54
+ const totalCount = treeCount + grassCount + weedCount;
55
+ const parsed = {
56
+ overall: {
57
+ count: totalCount,
58
+ level: (0, types_js_1.classifyLevel)(totalCount, this.thresholds.overall),
59
+ },
60
+ tree: {
61
+ count: treeCount,
62
+ level: (0, types_js_1.classifyLevel)(treeCount, this.thresholds.tree),
63
+ },
64
+ grass: {
65
+ count: grassCount,
66
+ level: (0, types_js_1.classifyLevel)(grassCount, this.thresholds.grass),
67
+ },
68
+ weed: {
69
+ count: weedCount,
70
+ level: (0, types_js_1.classifyLevel)(weedCount, this.thresholds.weed),
71
+ },
72
+ timestamp: item.updatedAt,
73
+ };
74
+ this.cache = parsed;
75
+ this.consecutiveFailures = 0;
76
+ this.log.debug(`Pollen data updated: overall=${totalCount} (${parsed.overall.level}), `
77
+ + `tree=${treeCount} (${parsed.tree.level}), `
78
+ + `grass=${grassCount} (${parsed.grass.level}), `
79
+ + `weed=${weedCount} (${parsed.weed.level})`);
80
+ return parsed;
81
+ }
82
+ catch (error) {
83
+ this.consecutiveFailures++;
84
+ this.log.warn(`Failed to fetch pollen data: ${error instanceof Error ? error.message : error}. `
85
+ + `Returning cached data. Consecutive failures: ${this.consecutiveFailures}`);
86
+ return this.cache;
87
+ }
88
+ }
89
+ getBackoffMultiplier() {
90
+ if (this.consecutiveFailures <= 1) {
91
+ return 1;
92
+ }
93
+ return Math.min(2 ** (this.consecutiveFailures - 1), 16);
94
+ }
95
+ getCachedData() {
96
+ return this.cache;
97
+ }
98
+ }
99
+ exports.PollenService = PollenService;
100
+ //# sourceMappingURL=pollenService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pollenService.js","sourceRoot":"","sources":["../src/pollenService.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ export declare const PLATFORM_NAME = "HomebridgePollen";
2
+ export declare const PLUGIN_NAME = "homebridge-pollen";
3
+ export declare const AMBEE_BASE_URL = "https://api.ambeedata.com";
4
+ export declare const DEFAULT_POLL_INTERVAL = 60;
5
+ export declare const MIN_POLL_INTERVAL = 15;
6
+ export interface ThresholdRange {
7
+ low: number;
8
+ high: number;
9
+ }
10
+ export interface Thresholds {
11
+ overall: ThresholdRange;
12
+ tree: ThresholdRange;
13
+ grass: ThresholdRange;
14
+ weed: ThresholdRange;
15
+ }
16
+ export declare const DEFAULT_THRESHOLDS: Thresholds;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_THRESHOLDS = exports.MIN_POLL_INTERVAL = exports.DEFAULT_POLL_INTERVAL = exports.AMBEE_BASE_URL = exports.PLUGIN_NAME = exports.PLATFORM_NAME = void 0;
4
+ exports.PLATFORM_NAME = 'HomebridgePollen';
5
+ exports.PLUGIN_NAME = 'homebridge-pollen';
6
+ exports.AMBEE_BASE_URL = 'https://api.ambeedata.com';
7
+ exports.DEFAULT_POLL_INTERVAL = 60; // minutes
8
+ exports.MIN_POLL_INTERVAL = 15; // minutes
9
+ exports.DEFAULT_THRESHOLDS = {
10
+ overall: { low: 50, high: 200 },
11
+ tree: { low: 15, high: 90 },
12
+ grass: { low: 20, high: 200 },
13
+ weed: { low: 10, high: 50 },
14
+ };
15
+ //# sourceMappingURL=settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.js","sourceRoot":"","sources":["../src/settings.ts"],"names":[],"mappings":";;;AAAa,QAAA,aAAa,GAAG,kBAAkB,CAAC;AACnC,QAAA,WAAW,GAAG,mBAAmB,CAAC;AAElC,QAAA,cAAc,GAAG,2BAA2B,CAAC;AAE7C,QAAA,qBAAqB,GAAG,EAAE,CAAC,CAAC,UAAU;AACtC,QAAA,iBAAiB,GAAG,EAAE,CAAC,CAAC,UAAU;AAclC,QAAA,kBAAkB,GAAe;IAC5C,OAAO,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;IAC/B,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;IAC3B,KAAK,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE;IAC7B,IAAI,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE;CAC5B,CAAC"}
@@ -0,0 +1,78 @@
1
+ import type { PlatformConfig } from 'homebridge';
2
+ import type { Thresholds } from './settings.js';
3
+ export interface PollenConfig extends PlatformConfig {
4
+ apiKey: string;
5
+ location: string;
6
+ pollInterval?: number;
7
+ enableCategorySensors?: boolean;
8
+ enableAirQualitySensor?: boolean;
9
+ thresholds?: Partial<{
10
+ overall: Partial<{
11
+ low: number;
12
+ high: number;
13
+ }>;
14
+ tree: Partial<{
15
+ low: number;
16
+ high: number;
17
+ }>;
18
+ grass: Partial<{
19
+ low: number;
20
+ high: number;
21
+ }>;
22
+ weed: Partial<{
23
+ low: number;
24
+ high: number;
25
+ }>;
26
+ }>;
27
+ }
28
+ export interface AmbeePollenCount {
29
+ grass_pollen: number;
30
+ tree_pollen: number;
31
+ weed_pollen: number;
32
+ }
33
+ export interface AmbeePollenRisk {
34
+ grass_pollen: string;
35
+ tree_pollen: string;
36
+ weed_pollen: string;
37
+ }
38
+ export interface AmbeePollenSpecies {
39
+ Grass: Record<string, number>;
40
+ Tree: Record<string, number>;
41
+ Weed: Record<string, number>;
42
+ }
43
+ export interface AmbeePollenDataItem {
44
+ Count: AmbeePollenCount;
45
+ Risk: AmbeePollenRisk;
46
+ Species: AmbeePollenSpecies;
47
+ updatedAt: string;
48
+ }
49
+ export interface AmbeePollenResponse {
50
+ message: string;
51
+ data: AmbeePollenDataItem[];
52
+ }
53
+ export type PollenLevel = 'High' | 'Medium' | 'Low';
54
+ export type PollenCategory = 'overall' | 'tree' | 'grass' | 'weed';
55
+ export interface CategoryData {
56
+ count: number;
57
+ level: PollenLevel;
58
+ }
59
+ export interface ParsedPollenData {
60
+ overall: CategoryData;
61
+ tree: CategoryData;
62
+ grass: CategoryData;
63
+ weed: CategoryData;
64
+ timestamp: string;
65
+ }
66
+ export type AccessoryType = 'contact' | 'airquality';
67
+ export interface AccessoryDefinition {
68
+ id: string;
69
+ name: string;
70
+ type: AccessoryType;
71
+ level?: PollenLevel;
72
+ category?: PollenCategory;
73
+ }
74
+ export declare function classifyLevel(count: number, thresholds: {
75
+ low: number;
76
+ high: number;
77
+ }): PollenLevel;
78
+ export declare function resolveThresholds(config: PollenConfig, defaults: Thresholds): Thresholds;
package/dist/types.js ADDED
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyLevel = classifyLevel;
4
+ exports.resolveThresholds = resolveThresholds;
5
+ function classifyLevel(count, thresholds) {
6
+ if (count >= thresholds.high) {
7
+ return 'High';
8
+ }
9
+ if (count <= thresholds.low) {
10
+ return 'Low';
11
+ }
12
+ return 'Medium';
13
+ }
14
+ function resolveThresholds(config, defaults) {
15
+ const overrides = config.thresholds;
16
+ if (!overrides) {
17
+ return defaults;
18
+ }
19
+ return {
20
+ overall: { ...defaults.overall, ...overrides.overall },
21
+ tree: { ...defaults.tree, ...overrides.tree },
22
+ grass: { ...defaults.grass, ...overrides.grass },
23
+ weed: { ...defaults.weed, ...overrides.weed },
24
+ };
25
+ }
26
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":";;AAyEA,sCAQC;AAED,8CAWC;AArBD,SAAgB,aAAa,CAAC,KAAa,EAAE,UAAyC;IACpF,IAAI,KAAK,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC;QAC7B,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,KAAK,IAAI,UAAU,CAAC,GAAG,EAAE,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAgB,iBAAiB,CAAC,MAAoB,EAAE,QAAoB;IAC1E,MAAM,SAAS,GAAG,MAAM,CAAC,UAAU,CAAC;IACpC,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,QAAQ,CAAC;IAClB,CAAC;IACD,OAAO;QACL,OAAO,EAAE,EAAE,GAAG,QAAQ,CAAC,OAAO,EAAE,GAAG,SAAS,CAAC,OAAO,EAAE;QACtD,IAAI,EAAE,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE;QAC7C,KAAK,EAAE,EAAE,GAAG,QAAQ,CAAC,KAAK,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE;QAChD,IAAI,EAAE,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,IAAI,EAAE;KAC9C,CAAC;AACJ,CAAC"}
@@ -0,0 +1,10 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+
4
+ export default tseslint.config(
5
+ eslint.configs.recommended,
6
+ ...tseslint.configs.recommended,
7
+ {
8
+ ignores: ['dist/'],
9
+ },
10
+ );
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "homebridge-pollen",
3
+ "version": "1.0.0",
4
+ "description": "Homebridge plugin that exposes pollen levels as HomeKit sensors using the Ambee API",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "engines": {
8
+ "node": ">=20.0.0",
9
+ "homebridge": "^1.8.0 || ^2.0.0"
10
+ },
11
+ "keywords": [
12
+ "homebridge",
13
+ "homebridge-plugin",
14
+ "pollen",
15
+ "allergy",
16
+ "allergies"
17
+ ],
18
+ "scripts": {
19
+ "build": "rimraf ./dist && tsc",
20
+ "lint": "eslint src/",
21
+ "setup": "bash scripts/setup.sh",
22
+ "dev": "npm run build && npm link && nodemon",
23
+ "prepare": "husky",
24
+ "prepublishOnly": "npm run lint && npm run build"
25
+ },
26
+ "lint-staged": {
27
+ "src/**/*.ts": "eslint"
28
+ },
29
+ "devDependencies": {
30
+ "@eslint/js": "^9.18.0",
31
+ "@types/node": "^20.19.30",
32
+ "eslint": "^9.18.0",
33
+ "homebridge": "^1.8.0",
34
+ "husky": "^9.1.7",
35
+ "lint-staged": "^16.2.7",
36
+ "nodemon": "^3.1.9",
37
+ "rimraf": "^6.0.1",
38
+ "typescript": "^5.7.3",
39
+ "typescript-eslint": "^8.21.0"
40
+ }
41
+ }