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.
- package/.husky/pre-commit +1 -0
- package/CLAUDE.md +98 -0
- package/CONTRIBUTING.md +89 -0
- package/LICENSE +21 -0
- package/README.md +125 -0
- package/config.schema.json +118 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +146 -0
- package/dist/platform.js.map +1 -0
- package/dist/platformAccessory.d.ts +15 -0
- package/dist/platformAccessory.js +75 -0
- package/dist/platformAccessory.js.map +1 -0
- package/dist/pollenService.d.ts +15 -0
- package/dist/pollenService.js +100 -0
- package/dist/pollenService.js.map +1 -0
- package/dist/settings.d.ts +16 -0
- package/dist/settings.js +15 -0
- package/dist/settings.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.js +26 -0
- package/dist/types.js.map +1 -0
- package/eslint.config.mjs +10 -0
- package/package.json +41 -0
|
@@ -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
|
package/CONTRIBUTING.md
ADDED
|
@@ -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
|
+
[](https://www.npmjs.com/package/homebridge-pollen)
|
|
4
|
+
[](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
|
+
}
|
package/dist/index.d.ts
ADDED
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
|
+
}
|
package/dist/platform.js
ADDED
|
@@ -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;
|
package/dist/settings.js
ADDED
|
@@ -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"}
|
package/dist/types.d.ts
ADDED
|
@@ -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"}
|
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
|
+
}
|