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 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
- # Temporary Holding Version
1
+ # homebridge-intratone
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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":"AAIA,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAc1C,0FAA0F;AAC1F,MAAM,UAAU,WAAW,CAAC,IAAY;IACtC,MAAM,OAAO,GAAG,IAAI;SACjB,OAAO,CAAC,uBAAuB,EAAE,GAAG,CAAC;SACrC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,mCAAmC,EAAE,EAAE,CAAC,CAAC;IACpD,OAAO,OAAO,IAAI,gBAAgB,CAAC;AACrC,CAAC;AAED,2FAA2F;AAC3F,MAAM,UAAU,iBAAiB,CAAC,MAAc,EAAE,aAAsB;IACtE,OAAO,WAAW,CAAC,aAAa,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC7G,CAAC;AAID;;;GAGG;AACH,MAAM,OAAO,eAAe;IAOP;IAGA;IATF,OAAO,CAAU;IAC1B,MAAM,CAAS;IACf,KAAK,GAAU,QAAQ,CAAC;IACxB,UAAU,CAAkB;IAEpC,YACmB,SAA2C,EAC5D,MAAc,EACd,WAAmB,EACF,IAAyB;QAHzB,cAAS,GAAT,SAAS,CAAkC;QAG3C,SAAI,GAAJ,IAAI,CAAqB;QAE1C,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QACjD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QAErB,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,GAAG,IAAI,CAAC,IAAI,KAAK,QAAQ;YAC/C,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,OAAO,CAAC,aAAa,CAAC;YACnD,CAAC,CAAC,CAAC,OAAO,CAAC,aAAa,EAAE,OAAO,CAAC,gBAAgB,CAAC,CAAC;QACtD,MAAM,KAAK,GAAG,SAAS,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;QAC7C,IAAI,KAAK,EAAE,CAAC;YACV,SAAS,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,SAAS,CAAC,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QAEzF,IAAI,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAC3B,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;YACjG,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,eAAe,CAAC;iBAC3D,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;iBAC/B,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;YAC7C,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,mBAAmB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;QACxF,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;YACjG,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,eAAe,CAAC;iBAC3D,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;iBAC/B,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;QAC/C,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;QACjC,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAED,MAAM,CAAC,MAAc,EAAE,WAAmB;QACxC,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QACtD,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,GAAG,MAAM,CAAC;QACvC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,oBAAoB,CAAC;YACrD,EAAE,iBAAiB,CAAC,cAAc,CAAC,YAAY,EAAE,WAAW,CAAC;aAC5D,iBAAiB,CAAC,cAAc,CAAC,KAAK,EAAE,WAAW,MAAM,CAAC,WAAW,CAAC,WAAW,EAAE,GAAG,CAAC;aACvF,iBAAiB,CAAC,cAAc,CAAC,YAAY,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,OAAO,CAAC,iBAAiB,CAAC,cAAc,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACnE,CAAC;IAED,OAAO;QACL,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC;IAEO,WAAW,CAAC,KAA0B;QAC5C,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAC7C,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ;YAC3C,CAAC,CAAC,KAAK,KAAK,cAAc,CAAC,eAAe,CAAC,IAAI;YAC/C,CAAC,CAAC,KAAK,KAAK,cAAc,CAAC,eAAe,CAAC,SAAS,CAAC;QAEvD,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC;aAAM,IAAI,IAAI,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;YACjC,iFAAiF;YACjF,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;YAC9B,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,yFAAyF;IACjF,OAAO;QACb,IAAI,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC5B,OAAO,CAAC,iEAAiE;QAC3E,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,WAAW,CAAC;QACxC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,WAAW,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAC9B,GAAG,EAAE;YACH,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,UAAU,CAAC,CAAC;YACtC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACtB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACjF,CAAC,EACD,CAAC,KAAc,EAAE,EAAE;YACjB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,IAAI,kBAAkB,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACrE,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QAC1B,CAAC,CACF,CAAC;IACJ,CAAC;IAEO,QAAQ,CAAC,KAAY;QAC3B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,IAAI,EAAE,CAAC;IACd,CAAC;IAEO,IAAI;QACV,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAC7C,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAChC,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,cAAc,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YACtF,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,cAAc,CAAC,gBAAgB,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;YACxF,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,cAAc,CAAC,mBAAmB,EAAE,KAAK,CAAC,CAAC;QAC/E,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,cAAc,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YACtF,IAAI,CAAC,OAAO,CAAC,oBAAoB,CAAC,cAAc,CAAC,gBAAgB,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IAEO,YAAY;QAClB,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAC7C,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,cAAc,CAAC,gBAAgB,CAAC;YAClE,OAAO,IAAI,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC;QACpF,CAAC;QACD,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,cAAc,CAAC,gBAAgB,CAAC;QAC/D,OAAO,IAAI,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;IACrD,CAAC;IAEO,WAAW;QACjB,MAAM,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;QAC7C,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,cAAc,CAAC,eAAe,CAAC;YACxD,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QACjD,CAAC;QACD,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,cAAc,CAAC,eAAe,CAAC;QAC9D,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;IACvD,CAAC;CACF"}
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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}