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 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
+ }