@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 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
+ [![npm](https://img.shields.io/npm/v/homebridge-pawport)](https://www.npmjs.com/package/homebridge-pawport)
4
+ [![npm](https://img.shields.io/npm/dt/homebridge-pawport)](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
+ }