@oblongmedulla/homebridge-pawport 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/LICENSE +21 -0
- package/README.md +120 -0
- package/config.schema.json +63 -0
- package/index.js +180 -0
- package/package.json +31 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 oblongmedulla
|
|
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,120 @@
|
|
|
1
|
+
# homebridge-pawport
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/homebridge-pawport)
|
|
4
|
+
[](https://www.npmjs.com/package/homebridge-pawport)
|
|
5
|
+
|
|
6
|
+
A [Homebridge](https://homebridge.io) plugin that exposes your [Pawport](https://pawport.com) smart pet door as a **Lock** in Apple Home. Lock and unlock your pet door from the Home app, Siri, or automations.
|
|
7
|
+
|
|
8
|
+
Supports **multiple doors** on a single account.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
### Via Homebridge UI (recommended)
|
|
15
|
+
Search for `homebridge-pawport` in the Homebridge plugin search and click Install.
|
|
16
|
+
|
|
17
|
+
### Manual
|
|
18
|
+
```bash
|
|
19
|
+
npm install -g homebridge-pawport
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Configuration
|
|
25
|
+
|
|
26
|
+
After installing, go to the **Plugins** tab in Homebridge UI, find `homebridge-pawport`, and click **Settings**. Add one entry per door with:
|
|
27
|
+
|
|
28
|
+
| Field | Description |
|
|
29
|
+
|-------|-------------|
|
|
30
|
+
| **Door Name** | Whatever you want to call it in Apple Home (e.g. "Dog Door") |
|
|
31
|
+
| **Auth Token** | Your `x-auth-token` from the Pawport app (see below) |
|
|
32
|
+
| **Door ID** | The UUID of your door (see below) |
|
|
33
|
+
|
|
34
|
+
### Manual config.json example
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"platforms": [
|
|
39
|
+
{
|
|
40
|
+
"platform": "PawportPlatform",
|
|
41
|
+
"name": "Pawport",
|
|
42
|
+
"doors": [
|
|
43
|
+
{
|
|
44
|
+
"name": "Dog Door",
|
|
45
|
+
"authToken": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
46
|
+
"doorID": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"name": "Cat Flap",
|
|
50
|
+
"authToken": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
51
|
+
"doorID": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Finding Your Credentials
|
|
62
|
+
|
|
63
|
+
You'll need to intercept your Pawport app's network traffic once to grab your `authToken` and `doorID`. Here's how:
|
|
64
|
+
|
|
65
|
+
### What you need
|
|
66
|
+
- An iPhone with the Pawport app installed
|
|
67
|
+
- [Proxyman](https://apps.apple.com/us/app/proxyman-network-debug-tool/id1551292695) (free) or [Stream](https://apps.apple.com/us/app/stream-http-debugging-proxy/id1312141691) (~$4.99) from the App Store
|
|
68
|
+
|
|
69
|
+
### Steps
|
|
70
|
+
|
|
71
|
+
1. Install Proxyman or Stream on your iPhone
|
|
72
|
+
2. Follow the in-app instructions to install the SSL certificate (Settings → trust the cert)
|
|
73
|
+
3. Start capturing traffic
|
|
74
|
+
4. Open the Pawport app, log in, and lock/unlock the door once
|
|
75
|
+
5. Stop capture and filter by `api.app.pawport.com`
|
|
76
|
+
6. Tap on a **sendDoorCommand** request
|
|
77
|
+
7. View the **Request** tab / Raw headers
|
|
78
|
+
|
|
79
|
+
You're looking for:
|
|
80
|
+
- **`x-auth-token`** header → this is your `authToken`
|
|
81
|
+
- **`doorID`** field in the request body → this is your `doorID`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## How It Works
|
|
86
|
+
|
|
87
|
+
This plugin communicates with the Pawport cloud API using the same GraphQL endpoint as the official iOS app. The door appears as a **Lock Mechanism** in HomeKit, which means you can:
|
|
88
|
+
|
|
89
|
+
- Lock / unlock from the Home app
|
|
90
|
+
- Use Siri ("Hey Siri, lock the dog door")
|
|
91
|
+
- Create automations (e.g. lock at sunset, unlock at 7am)
|
|
92
|
+
- Include it in Home scenes
|
|
93
|
+
|
|
94
|
+
> **Note:** This plugin is not affiliated with or endorsed by Pawport. Use at your own risk.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Troubleshooting
|
|
99
|
+
|
|
100
|
+
**Door shows as "No Response"**
|
|
101
|
+
- Check that your `authToken` is correct and hasn't expired
|
|
102
|
+
- Make sure your Homebridge server has internet access
|
|
103
|
+
|
|
104
|
+
**Command fails silently**
|
|
105
|
+
- Check the Homebridge logs for error details
|
|
106
|
+
- Try locking/unlocking from the Pawport app to confirm the door is online
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Contributing
|
|
111
|
+
|
|
112
|
+
Pull requests welcome! Please open an issue first to discuss what you'd like to change.
|
|
113
|
+
|
|
114
|
+
[GitHub Repository](https://github.com/oblongmedulla/homebridge-pawport)
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## License
|
|
119
|
+
|
|
120
|
+
MIT © [oblongmedulla](https://github.com/oblongmedulla)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "PawportPlatform",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": false,
|
|
5
|
+
"headerDisplay": "Control your Pawport smart pet door(s) from Apple Home. [Get your auth token and door ID →](https://github.com/oblongmedulla/homebridge-pawport#finding-your-credentials)",
|
|
6
|
+
"footerDisplay": "For help, visit the [GitHub repo](https://github.com/oblongmedulla/homebridge-pawport).",
|
|
7
|
+
"schema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"title": "Platform Name",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"default": "Pawport",
|
|
14
|
+
"required": true
|
|
15
|
+
},
|
|
16
|
+
"doors": {
|
|
17
|
+
"title": "Doors",
|
|
18
|
+
"type": "array",
|
|
19
|
+
"required": true,
|
|
20
|
+
"minItems": 1,
|
|
21
|
+
"items": {
|
|
22
|
+
"title": "Door",
|
|
23
|
+
"type": "object",
|
|
24
|
+
"properties": {
|
|
25
|
+
"name": {
|
|
26
|
+
"title": "Door Name",
|
|
27
|
+
"type": "string",
|
|
28
|
+
"placeholder": "e.g. Back Door, Dog Door",
|
|
29
|
+
"required": true
|
|
30
|
+
},
|
|
31
|
+
"authToken": {
|
|
32
|
+
"title": "Auth Token (x-auth-token)",
|
|
33
|
+
"type": "string",
|
|
34
|
+
"placeholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
35
|
+
"required": true,
|
|
36
|
+
"description": "Your Pawport account auth token. See README for how to find this."
|
|
37
|
+
},
|
|
38
|
+
"doorID": {
|
|
39
|
+
"title": "Door ID",
|
|
40
|
+
"type": "string",
|
|
41
|
+
"placeholder": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
42
|
+
"required": true,
|
|
43
|
+
"description": "The UUID of your Pawport door. See README for how to find this."
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"layout": [
|
|
51
|
+
{
|
|
52
|
+
"key": "doors",
|
|
53
|
+
"type": "array",
|
|
54
|
+
"title": "Doors",
|
|
55
|
+
"buttonText": "Add Door",
|
|
56
|
+
"items": [
|
|
57
|
+
"doors[].name",
|
|
58
|
+
"doors[].authToken",
|
|
59
|
+
"doors[].doorID"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLUGIN_NAME = 'homebridge-pawport';
|
|
4
|
+
const PLATFORM_NAME = 'PawportPlatform';
|
|
5
|
+
const GRAPHQL_URL = 'https://api.app.pawport.com/graphql';
|
|
6
|
+
|
|
7
|
+
const SEND_DOOR_COMMAND_MUTATION = `
|
|
8
|
+
mutation sendDoorCommand($command: String!, $doorID: String!, $arguments: json) {
|
|
9
|
+
sendDoorCommand(command: $command, doorID: $doorID, arguments: $arguments)
|
|
10
|
+
}
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
module.exports = (api) => {
|
|
14
|
+
api.registerPlatform(PLUGIN_NAME, PLATFORM_NAME, PawportPlatform);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ─── Platform ────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
class PawportPlatform {
|
|
20
|
+
constructor(log, config, api) {
|
|
21
|
+
this.log = log;
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.api = api;
|
|
24
|
+
this.accessories = [];
|
|
25
|
+
|
|
26
|
+
if (!config || !config.doors || config.doors.length === 0) {
|
|
27
|
+
this.log.warn('No doors configured. Add at least one door in your Homebridge config.');
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
this.api.on('didFinishLaunching', () => {
|
|
32
|
+
this.syncAccessories();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
syncAccessories() {
|
|
37
|
+
const configuredIDs = new Set(this.config.doors.map(d => d.doorID));
|
|
38
|
+
|
|
39
|
+
// Remove accessories that are no longer in config
|
|
40
|
+
this.accessories = this.accessories.filter(acc => {
|
|
41
|
+
if (!configuredIDs.has(acc.context.doorID)) {
|
|
42
|
+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [acc]);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Add or restore accessories for each configured door
|
|
49
|
+
for (const doorConfig of this.config.doors) {
|
|
50
|
+
if (!doorConfig.doorID || !doorConfig.authToken) {
|
|
51
|
+
this.log.warn(`Door "${doorConfig.name}" is missing doorID or authToken — skipping.`);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const uuid = this.api.hap.uuid.generate(doorConfig.doorID);
|
|
56
|
+
const existing = this.accessories.find(a => a.UUID === uuid);
|
|
57
|
+
|
|
58
|
+
if (existing) {
|
|
59
|
+
this.log.info(`Restoring door: ${doorConfig.name}`);
|
|
60
|
+
new PawportAccessory(this.log, existing, this.api, doorConfig);
|
|
61
|
+
} else {
|
|
62
|
+
this.log.info(`Adding door: ${doorConfig.name}`);
|
|
63
|
+
const accessory = new this.api.platformAccessory(doorConfig.name, uuid);
|
|
64
|
+
accessory.context.doorID = doorConfig.doorID;
|
|
65
|
+
new PawportAccessory(this.log, accessory, this.api, doorConfig);
|
|
66
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
configureAccessory(accessory) {
|
|
72
|
+
this.accessories.push(accessory);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Accessory ───────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
class PawportAccessory {
|
|
79
|
+
constructor(log, accessory, api, config) {
|
|
80
|
+
this.log = log;
|
|
81
|
+
this.accessory = accessory;
|
|
82
|
+
this.api = api;
|
|
83
|
+
this.config = config;
|
|
84
|
+
|
|
85
|
+
this.authToken = config.authToken;
|
|
86
|
+
this.doorID = config.doorID;
|
|
87
|
+
|
|
88
|
+
// Current state tracking
|
|
89
|
+
this.currentState = api.hap.Characteristic.LockCurrentState.SECURED;
|
|
90
|
+
this.targetState = api.hap.Characteristic.LockTargetState.SECURED;
|
|
91
|
+
|
|
92
|
+
// Set accessory info
|
|
93
|
+
this.accessory
|
|
94
|
+
.getService(api.hap.Service.AccessoryInformation)
|
|
95
|
+
.setCharacteristic(api.hap.Characteristic.Manufacturer, 'Pawport')
|
|
96
|
+
.setCharacteristic(api.hap.Characteristic.Model, 'Smart Pet Door')
|
|
97
|
+
.setCharacteristic(api.hap.Characteristic.SerialNumber, this.doorID);
|
|
98
|
+
|
|
99
|
+
// Set up lock service
|
|
100
|
+
this.lockService =
|
|
101
|
+
this.accessory.getService(api.hap.Service.LockMechanism) ||
|
|
102
|
+
this.accessory.addService(api.hap.Service.LockMechanism, config.name);
|
|
103
|
+
|
|
104
|
+
this.lockService
|
|
105
|
+
.getCharacteristic(api.hap.Characteristic.LockCurrentState)
|
|
106
|
+
.onGet(() => this.currentState);
|
|
107
|
+
|
|
108
|
+
this.lockService
|
|
109
|
+
.getCharacteristic(api.hap.Characteristic.LockTargetState)
|
|
110
|
+
.onGet(() => this.targetState)
|
|
111
|
+
.onSet(async (value) => {
|
|
112
|
+
this.targetState = value;
|
|
113
|
+
const shouldLock = value === api.hap.Characteristic.LockTargetState.SECURED;
|
|
114
|
+
this.log.info(`${config.name}: ${shouldLock ? 'Locking' : 'Unlocking'}...`);
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const success = await this.sendDoorCommand('door_lock', { locked: shouldLock });
|
|
118
|
+
|
|
119
|
+
if (success) {
|
|
120
|
+
this.currentState = shouldLock
|
|
121
|
+
? api.hap.Characteristic.LockCurrentState.SECURED
|
|
122
|
+
: api.hap.Characteristic.LockCurrentState.UNSECURED;
|
|
123
|
+
|
|
124
|
+
this.lockService
|
|
125
|
+
.getCharacteristic(api.hap.Characteristic.LockCurrentState)
|
|
126
|
+
.updateValue(this.currentState);
|
|
127
|
+
|
|
128
|
+
this.log.info(`${config.name}: ${shouldLock ? 'Locked ✓' : 'Unlocked ✓'}`);
|
|
129
|
+
} else {
|
|
130
|
+
throw new Error('API returned false');
|
|
131
|
+
}
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this.log.error(`${config.name}: Command failed — ${err.message}`);
|
|
134
|
+
// Revert target state to match current on failure
|
|
135
|
+
this.targetState = shouldLock
|
|
136
|
+
? api.hap.Characteristic.LockTargetState.UNSECURED
|
|
137
|
+
: api.hap.Characteristic.LockTargetState.SECURED;
|
|
138
|
+
this.lockService
|
|
139
|
+
.getCharacteristic(api.hap.Characteristic.LockTargetState)
|
|
140
|
+
.updateValue(this.targetState);
|
|
141
|
+
throw new api.hap.HapStatusError(api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async sendDoorCommand(command, args = {}) {
|
|
147
|
+
const body = {
|
|
148
|
+
operationName: 'sendDoorCommand',
|
|
149
|
+
variables: {
|
|
150
|
+
doorID: this.doorID,
|
|
151
|
+
command,
|
|
152
|
+
arguments: args,
|
|
153
|
+
},
|
|
154
|
+
query: SEND_DOOR_COMMAND_MUTATION,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const response = await fetch(GRAPHQL_URL, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: {
|
|
160
|
+
'Content-Type': 'application/json',
|
|
161
|
+
'x-auth-token': this.authToken,
|
|
162
|
+
'Accept': '*/*',
|
|
163
|
+
'User-Agent': 'homebridge-pawport',
|
|
164
|
+
},
|
|
165
|
+
body: JSON.stringify(body),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (!response.ok) {
|
|
169
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const data = await response.json();
|
|
173
|
+
|
|
174
|
+
if (data.errors) {
|
|
175
|
+
throw new Error(data.errors.map(e => e.message).join(', '));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return data?.data?.sendDoorCommand === true;
|
|
179
|
+
}
|
|
180
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oblongmedulla/homebridge-pawport",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Homebridge plugin for the Pawport smart pet door",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"homebridge-plugin",
|
|
9
|
+
"pawport",
|
|
10
|
+
"pet door",
|
|
11
|
+
"smart lock",
|
|
12
|
+
"homebridge"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0",
|
|
16
|
+
"homebridge": ">=1.3.0"
|
|
17
|
+
},
|
|
18
|
+
"author": "oblongmedulla",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/oblongmedulla/homebridge-pawport.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/oblongmedulla/homebridge-pawport/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/oblongmedulla/homebridge-pawport#readme",
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"homebridge": "^1.8.0"
|
|
30
|
+
}
|
|
31
|
+
}
|