homebridge-inco-cron 0.6.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/README.md +115 -0
- package/config.schema.json +36 -0
- package/controller-config.js +16 -0
- package/index.js +83 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# homebridge-inco-cron
|
|
2
|
+
|
|
3
|
+
Expose controller scheduling gates as Apple Home on/off switches.
|
|
4
|
+
|
|
5
|
+
## Requirements and installation
|
|
6
|
+
|
|
7
|
+
Requires Node.js 18 or newer, Homebridge 1.6 or newer, and `npm-inco-pkg ^1.3.0`.
|
|
8
|
+
The Homebridge host must be able to reach the controller over IPv4 UDP port 1964.
|
|
9
|
+
|
|
10
|
+
Install the published package through Homebridge Plugins, or run
|
|
11
|
+
`npm install homebridge-inco-cron` in the Homebridge installation directory.
|
|
12
|
+
|
|
13
|
+
For local development builds, run `npm pack` in this plugin's source directory and in
|
|
14
|
+
`npm-inco-pkg`. Copy the resulting archives to the Homebridge host, then install
|
|
15
|
+
both from the Homebridge installation directory using its Node/npm environment:
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm install /path/to/npm-inco-pkg-1.3.0.tgz /path/to/homebridge-inco-cron-VERSION.tgz
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Replace the archive paths and `VERSION` with the files you built. If the required
|
|
22
|
+
library is already installed, only the plugin archive is needed. Local versions
|
|
23
|
+
need not be published to the npm registry. Save configuration before restarting
|
|
24
|
+
Homebridge.
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Merge this example into the existing Homebridge configuration; it is a fragment,
|
|
29
|
+
not a replacement for the whole file. Preserve the bridge, other accessories and
|
|
30
|
+
other platforms. Add entries to existing arrays rather than creating duplicate
|
|
31
|
+
JSON keys.
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"inco": {
|
|
36
|
+
"controller": "10.0.0.3"
|
|
37
|
+
},
|
|
38
|
+
"platforms": [
|
|
39
|
+
{
|
|
40
|
+
"platform": "IncoCron",
|
|
41
|
+
"name": "INCO Cron",
|
|
42
|
+
"accessories": [
|
|
43
|
+
{
|
|
44
|
+
"name": "Parents automatic",
|
|
45
|
+
"path": "Home.Upper.Parents.Cron"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "Joel automatic",
|
|
49
|
+
"path": "Home.Upper.Joel.Cron"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Set **`inco.controller` once at the root of `config.json`**, shared by the light,
|
|
58
|
+
window-cover, temperature and cron plugins. In the Homebridge web interface,
|
|
59
|
+
edit this through the main **Config** JSON editor. It is not an individual plugin
|
|
60
|
+
setting. A valid IPv4 address is required; missing or invalid configuration fails
|
|
61
|
+
explicitly. Remove old `controller` and `host` fields from INCO accessories and
|
|
62
|
+
platforms. Save and restart Homebridge after configuration changes.
|
|
63
|
+
|
|
64
|
+
## Controller behavior
|
|
65
|
+
|
|
66
|
+
The configured accessory path is used exactly as supplied:
|
|
67
|
+
|
|
68
|
+
| Operation | Controller call |
|
|
69
|
+
|---|---|
|
|
70
|
+
| Read enabled state | `GetVariable(path)`: 1 means enabled, 0 means disabled |
|
|
71
|
+
| Enable | `PutVariable(path, 1)` |
|
|
72
|
+
| Disable | `PutVariable(path, 0)` |
|
|
73
|
+
|
|
74
|
+
No suffix is appended and no procedure is called. Every write is followed by a
|
|
75
|
+
read of the same path to verify the requested state. Invalid values, read/write
|
|
76
|
+
errors and verification failures are reported to HomeKit. Startup does not write.
|
|
77
|
+
The controller should make writes immediately readable and persist them as needed.
|
|
78
|
+
|
|
79
|
+
The controller owns schedules and executes their commands. A switch can control
|
|
80
|
+
a home, floor or room gate, rather than one scheduled action. For example,
|
|
81
|
+
`Home.Upper.Parents.Cron` reflects the Parents gate only: if `Home.Cron` or another
|
|
82
|
+
ancestor gate is zero, scheduling can remain suppressed while Parents is on.
|
|
83
|
+
|
|
84
|
+
Entries are configured manually. There is no schedule discovery or background
|
|
85
|
+
polling; external changes appear when HomeKit next requests the state. Requests
|
|
86
|
+
for each switch are serialized, with a five-second deadline per transport call.
|
|
87
|
+
|
|
88
|
+
## Migration and identity
|
|
89
|
+
|
|
90
|
+
Cron entries belong under the `IncoCron` platform, not the root `accessories`
|
|
91
|
+
array. Remove old standalone IncoCron entries when migrating, retaining unrelated
|
|
92
|
+
accessories. The old `.Enabled`/`.Enable()`/`.Disable()` interface is unsupported.
|
|
93
|
+
Use the actual readable/writable gate path.
|
|
94
|
+
|
|
95
|
+
Each switch uses its path as its internal identity unless `uuid_base` is supplied.
|
|
96
|
+
Keep paths or explicit IDs stable to preserve pairing identity. Duplicate paths
|
|
97
|
+
within the platform are rejected. Duplicate display names can use different paths.
|
|
98
|
+
|
|
99
|
+
## Frame limits and troubleshooting
|
|
100
|
+
|
|
101
|
+
`npm-inco-pkg` enforces a **494-byte complete encoded INCO message** limit,
|
|
102
|
+
including headers, checksums, escape bytes and terminators. IP/UDP headers are
|
|
103
|
+
outside this count. There is no arbitrary 36-character path cap. Operation
|
|
104
|
+
suffixes and numeric write data also consume frame space.
|
|
105
|
+
|
|
106
|
+
Oversized requests fail before transmission with `INCO_FRAME_TOO_LARGE`.
|
|
107
|
+
If a device is unavailable, check the root controller address, the actual INCO
|
|
108
|
+
variable or procedure path, controller connectivity, and the Homebridge log.
|
|
109
|
+
All INCO plugins must use the shared controller setting.
|
|
110
|
+
|
|
111
|
+
## Development checks
|
|
112
|
+
|
|
113
|
+
Run `npm test` from this plugin's source directory. This runs the plugin's mocked behavior tests.
|
|
114
|
+
Tests do not exercise a real controller. Repository-level INCO transport and
|
|
115
|
+
error-propagation tests provide additional coverage.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "IncoCron",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"singular": true,
|
|
5
|
+
"headerDisplay": "Controller address is shared by all INCO plugins. Set inco.controller once in the main Homebridge config.json.",
|
|
6
|
+
"schema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"default": "INCO Cron"
|
|
12
|
+
},
|
|
13
|
+
"accessories": {
|
|
14
|
+
"title": "Cron accessories",
|
|
15
|
+
"type": "array",
|
|
16
|
+
"items": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {
|
|
19
|
+
"name": {
|
|
20
|
+
"title": "Name",
|
|
21
|
+
"type": "string",
|
|
22
|
+
"required": true
|
|
23
|
+
},
|
|
24
|
+
"path": {
|
|
25
|
+
"title": "Cron INCO Path",
|
|
26
|
+
"type": "string",
|
|
27
|
+
"required": true,
|
|
28
|
+
"pattern": "^[A-Za-z0-9_.]+$",
|
|
29
|
+
"description": "INCO path. Complete encoded requests, including any operation suffixes, must fit the 494-byte controller frame limit; checked by npm-inco-pkg."
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Read the same root setting in every INCO plugin, including child bridges.
|
|
4
|
+
module.exports = function configureController(homebridge, inco) {
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const net = require('net');
|
|
7
|
+
const config = JSON.parse(fs.readFileSync(homebridge.user.configPath(), 'utf8'));
|
|
8
|
+
const controller = config.inco && config.inco.controller;
|
|
9
|
+
if (typeof controller !== 'string' || net.isIP(controller) !== 4) {
|
|
10
|
+
throw new Error('Set inco.controller to the controller IPv4 address in Homebridge config.json');
|
|
11
|
+
}
|
|
12
|
+
// Each plugin initializes its transport from exactly the same source.
|
|
13
|
+
// No accessory constructor may override this address.
|
|
14
|
+
inco.init(controller);
|
|
15
|
+
return controller;
|
|
16
|
+
};
|
package/index.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = function register(homebridge) {
|
|
4
|
+
const inco = require('npm-inco-pkg');
|
|
5
|
+
require('./controller-config')(homebridge, inco);
|
|
6
|
+
const { Service, Characteristic } = homebridge.hap;
|
|
7
|
+
|
|
8
|
+
class IncoCron {
|
|
9
|
+
constructor(log, config) {
|
|
10
|
+
this.log = log;
|
|
11
|
+
this.name = config.name;
|
|
12
|
+
this.path = config.path;
|
|
13
|
+
if (!this.name || !/^[A-Za-z0-9_.]+$/.test(this.path || '')) {
|
|
14
|
+
throw new Error('Cron name and a valid INCO path are required');
|
|
15
|
+
}
|
|
16
|
+
this.uuid_base = config.uuid_base || config.path;
|
|
17
|
+
this.pending = Promise.resolve();
|
|
18
|
+
this.service = new Service.Switch(this.name);
|
|
19
|
+
this.service.getCharacteristic(Characteristic.On)
|
|
20
|
+
.onGet(() => this.enqueue(() => this.readEnabled()))
|
|
21
|
+
.onSet(value => this.enqueue(async () => {
|
|
22
|
+
await this.request('putVariable', this.path, value ? 1 : 0);
|
|
23
|
+
const enabled = await this.readEnabled();
|
|
24
|
+
if (enabled !== Boolean(value)) throw new Error('Controller did not apply cron enabled state');
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
enqueue(operation) {
|
|
29
|
+
const result = this.pending.then(operation);
|
|
30
|
+
this.pending = result.catch(() => {});
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
request(method, path, ...args) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
// Deadline also covers time spent queued behind other accessories.
|
|
37
|
+
const timer = setTimeout(() => reject(new Error('INCO request timed out: ' + path)), 5000);
|
|
38
|
+
try {
|
|
39
|
+
inco[method](path, ...args, (err, value) => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
if (err) reject(err);
|
|
42
|
+
else resolve(value);
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
reject(err);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async readEnabled() {
|
|
52
|
+
const value = await this.request('getVariable', this.path);
|
|
53
|
+
if (value !== 0 && value !== 1) throw new Error('Cron value must be numeric 0 or 1');
|
|
54
|
+
return value === 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getServices() { return [this.service]; }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class IncoCronPlatform {
|
|
61
|
+
constructor(log, config) {
|
|
62
|
+
this.log = log;
|
|
63
|
+
this.config = config;
|
|
64
|
+
if ('controller' in config || 'host' in config) {
|
|
65
|
+
throw new Error('Set controller only in the root inco.controller setting');
|
|
66
|
+
}
|
|
67
|
+
const entries = config.accessories || [];
|
|
68
|
+
if (new Set(entries.map(entry => entry.path)).size !== entries.length) {
|
|
69
|
+
throw new Error('Duplicate cron accessory paths');
|
|
70
|
+
}
|
|
71
|
+
if (entries.some(entry => 'controller' in entry || 'host' in entry)) {
|
|
72
|
+
throw new Error('Set controller only in the root inco.controller setting');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
accessories(callback) {
|
|
77
|
+
callback((this.config.accessories || []).map(entry =>
|
|
78
|
+
new IncoCron(this.log, entry)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
homebridge.registerPlatform('homebridge-inco-cron', 'IncoCron', IncoCronPlatform);
|
|
83
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-inco-cron",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Enable and disable controller-owned INCO cron entries in Apple Home",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"index.js",
|
|
8
|
+
"config.schema.json",
|
|
9
|
+
"README.md",
|
|
10
|
+
"controller-config.js"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"test": "node --test test.js"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18",
|
|
17
|
+
"homebridge": ">=1.6.0"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"homebridge-plugin",
|
|
21
|
+
"INCO",
|
|
22
|
+
"cron"
|
|
23
|
+
],
|
|
24
|
+
"author": "Christoph Hirzel",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"npm-inco-pkg": "^1.3.0"
|
|
28
|
+
}
|
|
29
|
+
}
|