homebridge-intratone 0.0.0-stage → 0.2.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 +174 -2
- package/config.schema.json +108 -0
- package/dist/accessAccessory.js +133 -0
- package/dist/accessAccessory.js.map +1 -0
- package/dist/cli.js +169 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +80 -0
- package/dist/config.js.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/intratoneClient.js +392 -0
- package/dist/intratoneClient.js.map +1 -0
- package/dist/matterDoors.js +204 -0
- package/dist/matterDoors.js.map +1 -0
- package/dist/platform.js +309 -0
- package/dist/platform.js.map +1 -0
- package/dist/settings.js +5 -0
- package/dist/settings.js.map +1 -0
- package/dist/storage.js +87 -0
- package/dist/storage.js.map +1 -0
- package/dist/util.js +80 -0
- package/dist/util.js.map +1 -0
- package/package.json +50 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 jtoucas-DIMO
|
|
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
CHANGED
|
@@ -1,3 +1,175 @@
|
|
|
1
|
-
#
|
|
1
|
+
# homebridge-intratone
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Homebridge dynamic-platform plugin that opens **Intratone / Cogelec** doors and gates from
|
|
4
|
+
HomeKit and Matter, using the same REST calls as the official app. Every door your account can
|
|
5
|
+
open over the API (`data`/4G mobipass and `ble` modes) becomes a HomeKit lock (or garage door)
|
|
6
|
+
and, on a bridge with Matter enabled, a Matter door lock for Google Home, Alexa, SmartThings or
|
|
7
|
+
Apple Home.
|
|
8
|
+
|
|
9
|
+
Phase 1 covers remote opening only. Doorbell, video and two-way audio (SIP + FCM) are not
|
|
10
|
+
implemented.
|
|
11
|
+
|
|
12
|
+
> **Unofficial.** This plugin is not affiliated with or endorsed by Intratone or Cogelec. It
|
|
13
|
+
> talks to the private API of their app, which can change or stop working at any time.
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
- Registers as a device with `POST /api/auth/device` and keeps the 7-day JWT fresh. It checks
|
|
18
|
+
every 12 h, refreshes when less than a day is left, and refreshes once on a refused call.
|
|
19
|
+
Concurrent callers share a single refresh.
|
|
20
|
+
- Lists doors with `GET /api/access` at startup and then every hour (configurable). Doors are
|
|
21
|
+
added and removed as residences change. An empty list never removes existing doors, so a
|
|
22
|
+
server hiccup can't wipe your HomeKit rooms and automations.
|
|
23
|
+
- Opens with `POST /api/access/open/clemobil` (`phonenumber`, `access_id`). This endpoint also
|
|
24
|
+
handles `data` and `ble` doors.
|
|
25
|
+
- `clemobil` (2G) doors are skipped. The app opens those by placing a GSM call, which a server
|
|
26
|
+
can't do.
|
|
27
|
+
- The open is a stateless pulse with no real door state to read. A lock shows **Unlocked** for
|
|
28
|
+
`pulse_seconds`, then returns to **Locked**. A failed open goes straight back to Locked and
|
|
29
|
+
logs the HTTP status and body.
|
|
30
|
+
- On HTTP 429 the plugin backs off, honouring `Retry-After` or else waiting 1 min, doubling up
|
|
31
|
+
to 15 min, and sends nothing during the backoff.
|
|
32
|
+
|
|
33
|
+
## Install
|
|
34
|
+
|
|
35
|
+
In the Homebridge UI, open **Plugins**, search for **intratone** and click **Install**. This
|
|
36
|
+
works the same in the official `homebridge/homebridge` Docker image (Synology, Unraid, …).
|
|
37
|
+
|
|
38
|
+
Without the UI:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install -g homebridge-intratone
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then fill in the plugin settings (see below) and enable **child bridge** for it (plugin
|
|
45
|
+
settings → *Bridge Settings*), so a fault here can't take the main bridge down.
|
|
46
|
+
|
|
47
|
+
### Configuration
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"platform": "Intratone",
|
|
52
|
+
"name": "Intratone",
|
|
53
|
+
"app_id": "<from the official app>",
|
|
54
|
+
"app_token": "<from the official app>",
|
|
55
|
+
"tel": "0612345678",
|
|
56
|
+
"indicatif": "33",
|
|
57
|
+
"service": "lock",
|
|
58
|
+
"_bridge": { "username": "0E:9A:…", "port": 51830 }
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
| key | default | notes |
|
|
63
|
+
| ------------------------ | -------- | ----- |
|
|
64
|
+
| `app_id`, `app_token` | required | Hardcoded in the official app and not shipped with this plugin. See [Getting app_id and app_token](#getting-app_id-and-app_token). |
|
|
65
|
+
| `tel` | required | Phone number of the Intratone account. National (`06…`) or international (`+33 6…`). Several spellings are tried, and the one that works is remembered. |
|
|
66
|
+
| `indicatif` | `33` | Country code. |
|
|
67
|
+
| `device_id` | generated | Leave empty to generate a UUID that is stored and reused forever. Set it to reuse an existing phone's registration (`device_id` claim of that phone's JWT). |
|
|
68
|
+
| `service` | `lock` | `lock` (LockMechanism) or `garage` (GarageDoorOpener). |
|
|
69
|
+
| `pulse_seconds` | `2` | How long a door shows as unlocked/open after a successful open. |
|
|
70
|
+
| `access_refresh_minutes` | `60` | How often the door list is re-fetched. |
|
|
71
|
+
| `user_agent` | `ios` | `ios` or `android` client identity. |
|
|
72
|
+
| `app_version` | `4.6.4` | `appversion` sent at login. |
|
|
73
|
+
| `debug` | `false` | Logs every request and response at info level, plus the raw door list once at startup. Tokens, app credentials and your phone number are masked. |
|
|
74
|
+
|
|
75
|
+
### Matter
|
|
76
|
+
|
|
77
|
+
Needs Homebridge 2. In the plugin settings, open *Bridge Settings*, tick **Enable Matter**,
|
|
78
|
+
pick a free Matter port, save and restart the child bridge. The Matter QR code and pairing code
|
|
79
|
+
then appear in the same *Bridge Settings* dialog; scan them from Google Home, Alexa, SmartThings
|
|
80
|
+
or any other Matter controller. Several controllers can share the same pairing.
|
|
81
|
+
|
|
82
|
+
- Every door becomes a Matter **door lock**, whatever `service` is set to: Matter controllers
|
|
83
|
+
have no gate opener. *Unlock* opens the door; it shows unlocked for `pulse_seconds`, then
|
|
84
|
+
locked again. A failed open is reported to the controller as an error.
|
|
85
|
+
- HomeKit keeps working on the same bridge. Pairing the Matter bridge into Apple Home as well
|
|
86
|
+
would show every door twice, so use Matter for the other ecosystems, or turn HomeKit off for
|
|
87
|
+
this bridge if you only want Matter.
|
|
88
|
+
- Some controllers restrict unlocking a door lock by voice, or ask for a PIN that this plugin
|
|
89
|
+
doesn't provide. Unlocking from the controller's app is unaffected.
|
|
90
|
+
- Homebridge's Matter support is experimental and not CSA certified, so controllers may warn
|
|
91
|
+
about an uncertified accessory while pairing.
|
|
92
|
+
|
|
93
|
+
The plugin keeps `device_id` and the current JWT in
|
|
94
|
+
`<homebridge storage>/intratone/<hash>.json` (mode 0600). Delete that file only if you want to
|
|
95
|
+
register as a new device.
|
|
96
|
+
|
|
97
|
+
### Getting app_id and app_token
|
|
98
|
+
|
|
99
|
+
The official app sends both values only when it logs in (`POST /api/auth/device`). To read
|
|
100
|
+
them, capture your own phone's traffic with an HTTPS proxy such as
|
|
101
|
+
[mitmproxy](https://mitmproxy.org/):
|
|
102
|
+
|
|
103
|
+
1. Route the phone through the proxy and trust the proxy's certificate on the phone.
|
|
104
|
+
2. Make the app log in. It does this on its own about once a week, when its token expires, or
|
|
105
|
+
at once after you delete and reinstall it and register again.
|
|
106
|
+
3. In the capture, open the `POST https://sip.intratone.info/api/auth/device` request. Its
|
|
107
|
+
form body contains `app_id` and `app_token`, plus the `device_id` and `tel` the app uses.
|
|
108
|
+
|
|
109
|
+
Reusing that `device_id` makes Homebridge act as your phone, so you don't need a mobipass
|
|
110
|
+
transfer. A capture contains live tokens: keep it private and delete it when you're done.
|
|
111
|
+
|
|
112
|
+
## Mobipass: taking the door key
|
|
113
|
+
|
|
114
|
+
Mobipass is single-owner: the key that opens `data` doors lives on one device at a time. If
|
|
115
|
+
another phone holds it, `GET /api/access` returns nothing. The log then says
|
|
116
|
+
*"The mobipass key for this account is active on another device"* and no door is exposed.
|
|
117
|
+
|
|
118
|
+
Transfer it with the bundled CLI. Run it as the user Homebridge runs as, so it uses the same
|
|
119
|
+
device_id and JWT:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
sudo -u homebridge homebridge-intratone activate -U /var/lib/homebridge
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
sudo -u homebridge homebridge-intratone verify --otp 123456 -U /var/lib/homebridge
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`activate` texts a code to the account phone. `verify` moves the key to Homebridge and
|
|
130
|
+
**revokes remote opening on the phone that held it**. The doors appear at the next access
|
|
131
|
+
refresh, or straight away if you restart the child bridge.
|
|
132
|
+
|
|
133
|
+
Other CLI commands:
|
|
134
|
+
|
|
135
|
+
| command | what it does |
|
|
136
|
+
| ------- | ------------ |
|
|
137
|
+
| `status` | Stored device_id, JWT expiry, last known mobipass flags |
|
|
138
|
+
| `login` | Authenticate now and print the mobipass flags |
|
|
139
|
+
| `list` | Every access, with ✔ openable / ✘ skipped and the reason |
|
|
140
|
+
| `open <access_id>` | Open one door from the shell, for testing |
|
|
141
|
+
|
|
142
|
+
Add `--debug` to any command to see the requests (tokens masked). Without `-U`, the CLI reads
|
|
143
|
+
`~/.homebridge/config.json`.
|
|
144
|
+
|
|
145
|
+
In the `homebridge/homebridge` Docker image, run the CLI from the Homebridge UI terminal:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
cd /homebridge && npx homebridge-intratone status -U /homebridge
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Development
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npm install
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npm test
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
npm run build
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`npm run lint` and `npm run typecheck` are also available. Link the build into a local
|
|
166
|
+
Homebridge with `npm link`.
|
|
167
|
+
|
|
168
|
+
The API notes come from the open-source Home Assistant integration
|
|
169
|
+
[GuiHash/ha-intratone](https://github.com/GuiHash/ha-intratone) and from a capture of the iOS
|
|
170
|
+
app. The server returns `openmode` in upper case (`["DATA"]`), and the plugin compares modes
|
|
171
|
+
case-insensitively.
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "Intratone",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"strictValidation": false,
|
|
6
|
+
"headerDisplay": "Opens Intratone / Cogelec doors from HomeKit. Run this plugin as a **child bridge**. If the mobipass key is held by another phone, transfer it with `homebridge-intratone activate` then `homebridge-intratone verify --otp <code>` (see README).",
|
|
7
|
+
"schema": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"name": {
|
|
11
|
+
"title": "Name",
|
|
12
|
+
"type": "string",
|
|
13
|
+
"default": "Intratone",
|
|
14
|
+
"required": true
|
|
15
|
+
},
|
|
16
|
+
"app_id": {
|
|
17
|
+
"title": "App ID",
|
|
18
|
+
"description": "Application id embedded in the official Intratone app.",
|
|
19
|
+
"type": "string",
|
|
20
|
+
"required": true
|
|
21
|
+
},
|
|
22
|
+
"app_token": {
|
|
23
|
+
"title": "App token",
|
|
24
|
+
"description": "Application token embedded in the official Intratone app.",
|
|
25
|
+
"type": "string",
|
|
26
|
+
"required": true
|
|
27
|
+
},
|
|
28
|
+
"tel": {
|
|
29
|
+
"title": "Phone number",
|
|
30
|
+
"description": "Phone number tied to the Intratone account, e.g. 0612345678.",
|
|
31
|
+
"type": "string",
|
|
32
|
+
"required": true
|
|
33
|
+
},
|
|
34
|
+
"indicatif": {
|
|
35
|
+
"title": "Country code",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"default": "33",
|
|
38
|
+
"pattern": "^[0-9]{1,4}$"
|
|
39
|
+
},
|
|
40
|
+
"device_id": {
|
|
41
|
+
"title": "Device ID",
|
|
42
|
+
"description": "Leave empty to generate one and keep it forever. To reuse an existing phone's registration, paste its device_id (found in its JWT).",
|
|
43
|
+
"type": "string",
|
|
44
|
+
"pattern": "^$|^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"
|
|
45
|
+
},
|
|
46
|
+
"service": {
|
|
47
|
+
"title": "HomeKit service",
|
|
48
|
+
"description": "HomeKit only. Over Matter every door is a door lock.",
|
|
49
|
+
"type": "string",
|
|
50
|
+
"default": "lock",
|
|
51
|
+
"oneOf": [
|
|
52
|
+
{ "title": "Lock (recommended)", "enum": ["lock"] },
|
|
53
|
+
{ "title": "Garage door", "enum": ["garage"] }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"pulse_seconds": {
|
|
57
|
+
"title": "Pulse duration (s)",
|
|
58
|
+
"description": "How long the door shows as unlocked/open after a successful command.",
|
|
59
|
+
"type": "number",
|
|
60
|
+
"default": 2,
|
|
61
|
+
"minimum": 1,
|
|
62
|
+
"maximum": 30
|
|
63
|
+
},
|
|
64
|
+
"access_refresh_minutes": {
|
|
65
|
+
"title": "Access list refresh (min)",
|
|
66
|
+
"description": "How often the list of doors is re-fetched to add or remove accessories.",
|
|
67
|
+
"type": "integer",
|
|
68
|
+
"default": 60,
|
|
69
|
+
"minimum": 15,
|
|
70
|
+
"maximum": 1440
|
|
71
|
+
},
|
|
72
|
+
"user_agent": {
|
|
73
|
+
"title": "Client identity",
|
|
74
|
+
"type": "string",
|
|
75
|
+
"default": "ios",
|
|
76
|
+
"oneOf": [
|
|
77
|
+
{ "title": "iOS app", "enum": ["ios"] },
|
|
78
|
+
{ "title": "Android app", "enum": ["android"] }
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"app_version": {
|
|
82
|
+
"title": "App version sent to the server",
|
|
83
|
+
"type": "string",
|
|
84
|
+
"default": "4.6.4"
|
|
85
|
+
},
|
|
86
|
+
"debug": {
|
|
87
|
+
"title": "Debug logging",
|
|
88
|
+
"description": "Logs request/response bodies (tokens masked) at info level.",
|
|
89
|
+
"type": "boolean",
|
|
90
|
+
"default": false
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"layout": [
|
|
95
|
+
"name",
|
|
96
|
+
{ "key": "app_id", "type": "password" },
|
|
97
|
+
{ "key": "app_token", "type": "password" },
|
|
98
|
+
{ "type": "flex", "flex-flow": "row wrap", "items": ["indicatif", "tel"] },
|
|
99
|
+
"service",
|
|
100
|
+
"pulse_seconds",
|
|
101
|
+
{
|
|
102
|
+
"type": "fieldset",
|
|
103
|
+
"title": "Advanced",
|
|
104
|
+
"expandable": true,
|
|
105
|
+
"items": ["device_id", "access_refresh_minutes", "user_agent", "app_version", "debug"]
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describeError } from './util.js';
|
|
2
|
+
/** HomeKit names must start and end with a letter or digit and avoid most punctuation. */
|
|
3
|
+
export function homeKitName(name) {
|
|
4
|
+
const cleaned = name
|
|
5
|
+
.replace(/[^\p{L}\p{N} '’.,-]/gu, ' ')
|
|
6
|
+
.replace(/\s+/g, ' ')
|
|
7
|
+
.replace(/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu, '');
|
|
8
|
+
return cleaned || 'Intratone door';
|
|
9
|
+
}
|
|
10
|
+
/** Prefix with the residence only when several residences would otherwise be ambiguous. */
|
|
11
|
+
export function accessDisplayName(access, withResidence) {
|
|
12
|
+
return homeKitName(withResidence && access.residence ? `${access.residence} ${access.name}` : access.name);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One Intratone access as a stateless pulse: the server gives no door state, so we show
|
|
16
|
+
* "unlocked/open" for `pulseMs` after a successful open and "locked/closed" otherwise.
|
|
17
|
+
*/
|
|
18
|
+
export class AccessAccessory {
|
|
19
|
+
accessory;
|
|
20
|
+
deps;
|
|
21
|
+
service;
|
|
22
|
+
access;
|
|
23
|
+
phase = 'closed';
|
|
24
|
+
pulseTimer;
|
|
25
|
+
constructor(accessory, access, displayName, deps) {
|
|
26
|
+
this.accessory = accessory;
|
|
27
|
+
this.deps = deps;
|
|
28
|
+
const { Service, Characteristic } = deps.api.hap;
|
|
29
|
+
this.access = access;
|
|
30
|
+
const [wanted, unwanted] = deps.kind === 'garage'
|
|
31
|
+
? [Service.GarageDoorOpener, Service.LockMechanism]
|
|
32
|
+
: [Service.LockMechanism, Service.GarageDoorOpener];
|
|
33
|
+
const stale = accessory.getService(unwanted);
|
|
34
|
+
if (stale) {
|
|
35
|
+
accessory.removeService(stale);
|
|
36
|
+
}
|
|
37
|
+
this.service = accessory.getService(wanted) ?? accessory.addService(wanted, displayName);
|
|
38
|
+
if (deps.kind === 'garage') {
|
|
39
|
+
this.service.getCharacteristic(Characteristic.CurrentDoorState).onGet(() => this.currentValue());
|
|
40
|
+
this.service.getCharacteristic(Characteristic.TargetDoorState)
|
|
41
|
+
.onGet(() => this.targetValue())
|
|
42
|
+
.onSet((value) => this.onTargetSet(value));
|
|
43
|
+
this.service.getCharacteristic(Characteristic.ObstructionDetected).onGet(() => false);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.service.getCharacteristic(Characteristic.LockCurrentState).onGet(() => this.currentValue());
|
|
47
|
+
this.service.getCharacteristic(Characteristic.LockTargetState)
|
|
48
|
+
.onGet(() => this.targetValue())
|
|
49
|
+
.onSet((value) => this.onTargetSet(value));
|
|
50
|
+
}
|
|
51
|
+
this.update(access, displayName);
|
|
52
|
+
this.push();
|
|
53
|
+
}
|
|
54
|
+
update(access, displayName) {
|
|
55
|
+
const { Service, Characteristic } = this.deps.api.hap;
|
|
56
|
+
this.access = access;
|
|
57
|
+
this.accessory.context.access = access;
|
|
58
|
+
this.accessory.getService(Service.AccessoryInformation)
|
|
59
|
+
?.setCharacteristic(Characteristic.Manufacturer, 'Intratone')
|
|
60
|
+
.setCharacteristic(Characteristic.Model, `Access (${access.primaryMode.toUpperCase()})`)
|
|
61
|
+
.setCharacteristic(Characteristic.SerialNumber, access.id);
|
|
62
|
+
this.service.setCharacteristic(Characteristic.Name, displayName);
|
|
63
|
+
}
|
|
64
|
+
dispose() {
|
|
65
|
+
clearTimeout(this.pulseTimer);
|
|
66
|
+
}
|
|
67
|
+
onTargetSet(value) {
|
|
68
|
+
const { Characteristic } = this.deps.api.hap;
|
|
69
|
+
const wantsOpen = this.deps.kind === 'garage'
|
|
70
|
+
? value === Characteristic.TargetDoorState.OPEN
|
|
71
|
+
: value === Characteristic.LockTargetState.UNSECURED;
|
|
72
|
+
if (wantsOpen) {
|
|
73
|
+
this.trigger();
|
|
74
|
+
}
|
|
75
|
+
else if (this.phase === 'open') {
|
|
76
|
+
// "Lock" pressed while the pulse is still showing: nothing to send, just settle.
|
|
77
|
+
clearTimeout(this.pulseTimer);
|
|
78
|
+
this.setPhase('closed');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Returns immediately so HomeKit shows "Unlocking…"; the outcome is pushed as state. */
|
|
82
|
+
trigger() {
|
|
83
|
+
if (this.phase !== 'closed') {
|
|
84
|
+
return; // A double tap while opening or showing open sends nothing more.
|
|
85
|
+
}
|
|
86
|
+
const name = this.accessory.displayName;
|
|
87
|
+
this.setPhase('opening');
|
|
88
|
+
this.deps.log.info(`${name}: opening`);
|
|
89
|
+
this.deps.open(this.access).then(() => {
|
|
90
|
+
this.deps.log.info(`${name}: opened`);
|
|
91
|
+
this.setPhase('open');
|
|
92
|
+
this.pulseTimer = setTimeout(() => this.setPhase('closed'), this.deps.pulseMs);
|
|
93
|
+
}, (error) => {
|
|
94
|
+
this.deps.log.error(`${name}: open failed: ${describeError(error)}`);
|
|
95
|
+
this.setPhase('closed');
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
setPhase(phase) {
|
|
99
|
+
this.phase = phase;
|
|
100
|
+
this.push();
|
|
101
|
+
}
|
|
102
|
+
push() {
|
|
103
|
+
const { Characteristic } = this.deps.api.hap;
|
|
104
|
+
if (this.deps.kind === 'garage') {
|
|
105
|
+
this.service.updateCharacteristic(Characteristic.TargetDoorState, this.targetValue());
|
|
106
|
+
this.service.updateCharacteristic(Characteristic.CurrentDoorState, this.currentValue());
|
|
107
|
+
this.service.updateCharacteristic(Characteristic.ObstructionDetected, false);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
this.service.updateCharacteristic(Characteristic.LockTargetState, this.targetValue());
|
|
111
|
+
this.service.updateCharacteristic(Characteristic.LockCurrentState, this.currentValue());
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
currentValue() {
|
|
115
|
+
const { Characteristic } = this.deps.api.hap;
|
|
116
|
+
if (this.deps.kind === 'garage') {
|
|
117
|
+
const { CLOSED, OPENING, OPEN } = Characteristic.CurrentDoorState;
|
|
118
|
+
return this.phase === 'open' ? OPEN : this.phase === 'opening' ? OPENING : CLOSED;
|
|
119
|
+
}
|
|
120
|
+
const { SECURED, UNSECURED } = Characteristic.LockCurrentState;
|
|
121
|
+
return this.phase === 'open' ? UNSECURED : SECURED;
|
|
122
|
+
}
|
|
123
|
+
targetValue() {
|
|
124
|
+
const { Characteristic } = this.deps.api.hap;
|
|
125
|
+
if (this.deps.kind === 'garage') {
|
|
126
|
+
const { CLOSED, OPEN } = Characteristic.TargetDoorState;
|
|
127
|
+
return this.phase === 'closed' ? CLOSED : OPEN;
|
|
128
|
+
}
|
|
129
|
+
const { SECURED, UNSECURED } = Characteristic.LockTargetState;
|
|
130
|
+
return this.phase === 'closed' ? SECURED : UNSECURED;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=accessAccessory.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accessAccessory.js","sourceRoot":"","sources":["../src/accessAccessory.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { parseArgs } from 'node:util';
|
|
6
|
+
import { openAccount, parseSettings } from './config.js';
|
|
7
|
+
import { decodeJwtPayload } from './intratoneClient.js';
|
|
8
|
+
import { PLATFORM_NAME } from './settings.js';
|
|
9
|
+
import { describeError, isRecord, str } from './util.js';
|
|
10
|
+
const USAGE = `Usage: homebridge-intratone <command> [options]
|
|
11
|
+
|
|
12
|
+
Commands:
|
|
13
|
+
status Show the stored device_id, JWT expiry and mobipass flags
|
|
14
|
+
login Authenticate now (auth/device) and show the mobipass flags
|
|
15
|
+
list List accesses and whether each can be opened here
|
|
16
|
+
open <access_id> Open one access (to test without HomeKit)
|
|
17
|
+
activate Start a mobipass transfer: texts a code to the account phone
|
|
18
|
+
verify --otp <code> Finish the transfer. Revokes remote opening on the previous device
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-U, --user-storage-path <dir> Homebridge storage directory (default ~/.homebridge)
|
|
22
|
+
-c, --config <file> config.json to read (default <storage>/config.json)
|
|
23
|
+
--otp <code> SMS code for verify
|
|
24
|
+
--debug Print requests and responses (tokens masked)
|
|
25
|
+
-h, --help Show this help
|
|
26
|
+
|
|
27
|
+
Run it as the user Homebridge runs as (e.g. sudo -u homebridge …), so it shares the
|
|
28
|
+
plugin's stored device_id and JWT.`;
|
|
29
|
+
function loadPlatformBlock(configPath) {
|
|
30
|
+
const config = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
31
|
+
const platforms = isRecord(config) && Array.isArray(config.platforms) ? config.platforms : [];
|
|
32
|
+
const block = platforms.find((platform) => isRecord(platform) && platform.platform === PLATFORM_NAME);
|
|
33
|
+
if (!block) {
|
|
34
|
+
throw new Error(`No "${PLATFORM_NAME}" platform block in ${configPath}`);
|
|
35
|
+
}
|
|
36
|
+
return block;
|
|
37
|
+
}
|
|
38
|
+
function formatFlags(flags) {
|
|
39
|
+
const verdict = flags.mobipassCompatible && !flags.mobipass
|
|
40
|
+
? 'key is held by another device: run `activate` then `verify --otp <code>`'
|
|
41
|
+
: flags.mobipass
|
|
42
|
+
? 'key is active on this device'
|
|
43
|
+
: 'account is not mobipass-only';
|
|
44
|
+
return [
|
|
45
|
+
` openingaccess ${flags.openingAccess}`,
|
|
46
|
+
` access_refresh ${flags.accessRefresh}`,
|
|
47
|
+
` mobipass_compatible ${flags.mobipassCompatible}`,
|
|
48
|
+
` mobipass ${flags.mobipass}`,
|
|
49
|
+
` → ${verdict}`,
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
function printAccesses(accesses) {
|
|
53
|
+
if (!accesses.openable.length && !accesses.skipped.length) {
|
|
54
|
+
console.log('No accesses returned.');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const access of accesses.openable) {
|
|
58
|
+
console.log(` ✔ ${access.id.padEnd(10)} ${access.residence} / ${access.name} [${access.modes.join(',')}]`);
|
|
59
|
+
}
|
|
60
|
+
for (const skipped of accesses.skipped) {
|
|
61
|
+
console.log(` ✘ ${(skipped.id || '?').padEnd(10)} ${skipped.residence} / ${skipped.name} [${skipped.mode || 'none'}: ${skipped.reason}]`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function run(command, args, otp, account) {
|
|
65
|
+
const { client, store } = account;
|
|
66
|
+
switch (command) {
|
|
67
|
+
case 'status': {
|
|
68
|
+
console.log(`device_id ${account.deviceId}${account.generatedDeviceId ? ' (just generated)' : ''}`);
|
|
69
|
+
console.log(`state file ${store.file}`);
|
|
70
|
+
const session = client.currentSession;
|
|
71
|
+
if (!session) {
|
|
72
|
+
console.log('session none (run `login`)');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const claims = decodeJwtPayload(session.jwt) ?? {};
|
|
76
|
+
const valid = client.hasValidSession() ? 'valid' : 'EXPIRED';
|
|
77
|
+
console.log(`session ${valid} until ${new Date(session.expiresAt).toISOString()}`);
|
|
78
|
+
console.log(`smartphone ${session.smartphoneId ?? str(claims.id)} (JWT device "${str(claims.device)}")`);
|
|
79
|
+
if (str(claims.device_id) && str(claims.device_id) !== account.deviceId) {
|
|
80
|
+
console.log(`WARNING JWT device_id ${str(claims.device_id)} differs from ${account.deviceId}`);
|
|
81
|
+
}
|
|
82
|
+
console.log(`mobipass flags (from the last login):\n${formatFlags(session.flags)}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
case 'login': {
|
|
86
|
+
const session = await client.refresh();
|
|
87
|
+
console.log(`Authenticated; JWT valid until ${new Date(session.expiresAt).toISOString()}`);
|
|
88
|
+
console.log(formatFlags(session.flags));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
case 'list': {
|
|
92
|
+
printAccesses((await client.listAccesses()).accesses);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
case 'open': {
|
|
96
|
+
const id = args[0];
|
|
97
|
+
if (!id) {
|
|
98
|
+
throw new Error('open needs an access_id (see `list`)');
|
|
99
|
+
}
|
|
100
|
+
const { accesses } = await client.listAccesses();
|
|
101
|
+
const access = accesses.openable.find((candidate) => candidate.id === id);
|
|
102
|
+
if (!access) {
|
|
103
|
+
throw new Error(`Access ${id} is not in the openable list (see \`list\`)`);
|
|
104
|
+
}
|
|
105
|
+
await client.open(access);
|
|
106
|
+
console.log(`Opened ${access.residence} / ${access.name}`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
case 'activate': {
|
|
110
|
+
const result = await client.mobipassActivate();
|
|
111
|
+
console.log(`${result.code ?? 'OK'}: an SMS code is on its way to the account phone.`);
|
|
112
|
+
console.log('Finish with: homebridge-intratone verify --otp <code>');
|
|
113
|
+
console.log('Verifying moves the key here and revokes remote opening on the device that holds it now.');
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
case 'verify': {
|
|
117
|
+
if (!otp) {
|
|
118
|
+
throw new Error('verify needs --otp <code>');
|
|
119
|
+
}
|
|
120
|
+
const result = await client.mobipassVerify(otp);
|
|
121
|
+
console.log(`${result.code ?? 'OK'}: mobipass transferred to this device.`);
|
|
122
|
+
const session = await client.refresh();
|
|
123
|
+
console.log(formatFlags(session.flags));
|
|
124
|
+
printAccesses((await client.listAccesses()).accesses);
|
|
125
|
+
console.log('Homebridge picks the doors up at its next access refresh, or restart the child bridge now.');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
default:
|
|
129
|
+
throw new Error(`Unknown command "${command}"\n\n${USAGE}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function main() {
|
|
133
|
+
const { values, positionals } = parseArgs({
|
|
134
|
+
allowPositionals: true,
|
|
135
|
+
options: {
|
|
136
|
+
'user-storage-path': { type: 'string', short: 'U' },
|
|
137
|
+
config: { type: 'string', short: 'c' },
|
|
138
|
+
otp: { type: 'string' },
|
|
139
|
+
debug: { type: 'boolean' },
|
|
140
|
+
help: { type: 'boolean', short: 'h' },
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
const [command, ...args] = positionals;
|
|
144
|
+
if (values.help || !command) {
|
|
145
|
+
console.log(USAGE);
|
|
146
|
+
return values.help ? 0 : 1;
|
|
147
|
+
}
|
|
148
|
+
const configPath = values.config
|
|
149
|
+
? resolve(values.config)
|
|
150
|
+
: join(resolve(values['user-storage-path'] ?? join(homedir(), '.homebridge')), 'config.json');
|
|
151
|
+
const storagePath = values['user-storage-path'] ? resolve(values['user-storage-path']) : dirname(configPath);
|
|
152
|
+
const { settings, errors } = parseSettings(loadPlatformBlock(configPath));
|
|
153
|
+
if (!settings) {
|
|
154
|
+
throw new Error(`Invalid ${PLATFORM_NAME} config in ${configPath}: ${errors.join('; ')}`);
|
|
155
|
+
}
|
|
156
|
+
const debug = values.debug || settings.debug;
|
|
157
|
+
const account = openAccount(settings, storagePath, {
|
|
158
|
+
debug: debug ? (message) => console.error(`[debug] ${message}`) : undefined,
|
|
159
|
+
});
|
|
160
|
+
await run(command, args, values.otp, account);
|
|
161
|
+
return 0;
|
|
162
|
+
}
|
|
163
|
+
main().then((code) => {
|
|
164
|
+
process.exitCode = code;
|
|
165
|
+
}, (error) => {
|
|
166
|
+
console.error(`Error: ${describeError(error)}`);
|
|
167
|
+
process.exitCode = 1;
|
|
168
|
+
});
|
|
169
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|