homebridge-philips-dline-sicp 0.1.3
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 +162 -0
- package/config.schema.json +116 -0
- package/index.js +576 -0
- package/package.json +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Thomas Dazy
|
|
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,162 @@
|
|
|
1
|
+
# homebridge-philips-dline-sicp
|
|
2
|
+
|
|
3
|
+
Homebridge plugin to control **Philips D‑Line** signage displays (e.g., **55BDL4511D/00**) over LAN using the **SICP** protocol on **TCP:5000**.
|
|
4
|
+
Exposes the device as a **HomeKit Television** (power + input selection), with optional per‑input **Switches**.
|
|
5
|
+
|
|
6
|
+
> ⚠️ This plugin is vendor‑unofficial. Keep your display on a trusted VLAN. No auth is implemented by the display on port 5000.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
- Power On/Off (SICP `0x18`).
|
|
10
|
+
- Input selection via configurable SICP codes (default HDMI1..4).
|
|
11
|
+
- Optional **per‑input switches** for quick Siri/Home automations.
|
|
12
|
+
- Multiple displays supported (platform plugin).
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
- Node.js >= 18.x
|
|
16
|
+
- Homebridge >= 1.6.0
|
|
17
|
+
- Philips D-Line display with **Network Control over RJ45** enabled (OSD menu), and reachable on the LAN.
|
|
18
|
+
- The display should listen on **TCP port 5000** (default for SICP over IP).
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
1. Copy this folder to your Homebridge environment, then:
|
|
22
|
+
```bash
|
|
23
|
+
cd homebridge-philips-dline-sicp
|
|
24
|
+
npm install
|
|
25
|
+
|
|
26
|
+
# For local development / manual install:
|
|
27
|
+
sudo npm i -g .
|
|
28
|
+
```
|
|
29
|
+
*or install from npm once published:*
|
|
30
|
+
```bash
|
|
31
|
+
sudo npm i -g homebridge-philips-dline-sicp
|
|
32
|
+
```
|
|
33
|
+
2. In Homebridge UI (Config), add the platform and displays as below.
|
|
34
|
+
|
|
35
|
+
## Configuration
|
|
36
|
+
Add to your Homebridge `config.json`:
|
|
37
|
+
|
|
38
|
+
```json
|
|
39
|
+
{
|
|
40
|
+
"platforms": [
|
|
41
|
+
{
|
|
42
|
+
"platform": "PhilipsDLinePlatform",
|
|
43
|
+
"displays": [
|
|
44
|
+
{
|
|
45
|
+
"name": "Salon TV",
|
|
46
|
+
"host": "192.168.1.120",
|
|
47
|
+
"port": 5000,
|
|
48
|
+
"monitorId": 1,
|
|
49
|
+
"includeGroup": true,
|
|
50
|
+
"groupId": 0,
|
|
51
|
+
"pollInterval": 10,
|
|
52
|
+
"exposeInputSwitches": true,
|
|
53
|
+
"inputs": [
|
|
54
|
+
{ "label": "HDMI 1", "code": "0x0D", "identifier": 1 },
|
|
55
|
+
{ "label": "HDMI 2", "code": "0x06", "identifier": 2 },
|
|
56
|
+
{ "label": "HDMI 3", "code": "0x0F", "identifier": 3 },
|
|
57
|
+
{ "label": "HDMI 4", "code": "0x19", "identifier": 4 }
|
|
58
|
+
],
|
|
59
|
+
"exposeBrightness": true
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Notes
|
|
69
|
+
- `monitorId`: use the Monitor ID set in the OSD (often 1). `0` means broadcast (typically no reply).
|
|
70
|
+
- `includeGroup`: some firmwares expect a *Group* byte; leave `true` unless you see no ACK, then try `false`.
|
|
71
|
+
- `inputs`: SICP input codes vary by model/firmware. The defaults work on many D-Line firmwares; adjust if needed.
|
|
72
|
+
- `pollInterval`: seconds between lightweight status refresh attempts. Set `0` to disable polling.
|
|
73
|
+
- `exposeInputSwitches`: creates a `Switch` per input (mutually exclusive) for simple automations.
|
|
74
|
+
- `exposeBrightness`: (Default `true`). Exposes a Lightbulb service for brightness control. **Warning**: If enabled, HomeKit may group this with other lights ("Turn on all lights" -> Turns on TV). Set to `false` if you experience this issue.
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
- Power: Use the Television tile → On/Off.
|
|
78
|
+
- Input: Change the **input** from the TV tile, or toggle the per‑input switches (if enabled).
|
|
79
|
+
- Siri: “Switch Salon TV to HDMI 2”, “Turn on Salon TV”.
|
|
80
|
+
|
|
81
|
+
## Troubleshooting
|
|
82
|
+
- **No response / timeouts**: check that the display answers on `tcp/5000` (`telnet IP 5000`), and that “Network control / RJ45” is enabled.
|
|
83
|
+
- **Input won’t change**: some displays require a short delay after power on; the plugin already waits ~300ms but you can increase it in code if needed.
|
|
84
|
+
- **Wrong input codes**: run with debugging, try other codes for `0xAC` (input set). If you have the SICP table for your firmware, copy the exact codes into `inputs`.
|
|
85
|
+
- **Security**: do not expose the port to the Internet. Restrict to your LAN/VLAN.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
MIT
|
|
89
|
+
|
|
90
|
+
## Volume & Brightness
|
|
91
|
+
|
|
92
|
+
You can control **volume** (HomeKit TelevisionSpeaker) and **brightness** (as a Lightbulb service named *Backlight*).
|
|
93
|
+
Because SICP codes may vary by firmware, you can choose between **absolute** and **relative** modes:
|
|
94
|
+
|
|
95
|
+
### Volume config
|
|
96
|
+
```json
|
|
97
|
+
"volume": {
|
|
98
|
+
"min": 0,
|
|
99
|
+
"max": 100,
|
|
100
|
+
"initial": 15,
|
|
101
|
+
"setCode": "0x44", // OPTIONAL: absolute set: [0x44, value 0..100]
|
|
102
|
+
"upCode": "0x45", // OPTIONAL: relative up: [0x45]
|
|
103
|
+
"downCode": "0x46", // OPTIONAL: relative down: [0x46]
|
|
104
|
+
"muteSetCode": "0x47", // OPTIONAL: absolute mute: [0x47, 0|1]
|
|
105
|
+
"muteToggleCode": "0x48", // OPTIONAL: toggle mute: [0x48]
|
|
106
|
+
"stepDelayMs": 120 // delay between relative steps
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
> Provide either `setCode` **or** (`upCode` and `downCode`). `muteSetCode` or `muteToggleCode` are optional.
|
|
110
|
+
|
|
111
|
+
### Brightness config
|
|
112
|
+
```json
|
|
113
|
+
"brightness": {
|
|
114
|
+
"min": 0,
|
|
115
|
+
"max": 100,
|
|
116
|
+
"initial": 50,
|
|
117
|
+
"setCode": "0x10", // OPTIONAL: absolute set: [0x10, value 0..100]
|
|
118
|
+
"upCode": "0x11", // OPTIONAL: relative up: [0x11]
|
|
119
|
+
"downCode": "0x12", // OPTIONAL: relative down: [0x12]
|
|
120
|
+
"stepDelayMs": 120
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
> If your firmware supports DDC-like absolute brightness, use `setCode` (often `0x32` or `0x10`). Otherwise use relative up/down.
|
|
124
|
+
> **Note**: If you do not configure brightness codes, the plugin will attempt a default SICP command (`0x32`) which works on many D-Line models.
|
|
125
|
+
|
|
126
|
+
### Example full display config
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"name": "Salon TV",
|
|
130
|
+
"host": "192.168.1.120",
|
|
131
|
+
"port": 5000,
|
|
132
|
+
"monitorId": 1,
|
|
133
|
+
"includeGroup": true,
|
|
134
|
+
"groupId": 0,
|
|
135
|
+
"pollInterval": 10,
|
|
136
|
+
"exposeInputSwitches": true,
|
|
137
|
+
"inputs": [
|
|
138
|
+
{ "label": "HDMI 1", "code": "0x0D", "identifier": 1 },
|
|
139
|
+
{ "label": "HDMI 2", "code": "0x06", "identifier": 2 },
|
|
140
|
+
{ "label": "HDMI 3", "code": "0x0F", "identifier": 3 },
|
|
141
|
+
{ "label": "HDMI 4", "code": "0x19", "identifier": 4 }
|
|
142
|
+
],
|
|
143
|
+
"volume": {
|
|
144
|
+
"min": 0,
|
|
145
|
+
"max": 60,
|
|
146
|
+
"initial": 10,
|
|
147
|
+
"setCode": "0x44",
|
|
148
|
+
"muteSetCode": "0x47"
|
|
149
|
+
},
|
|
150
|
+
"brightness": {
|
|
151
|
+
"min": 0,
|
|
152
|
+
"max": 100,
|
|
153
|
+
"initial": 50,
|
|
154
|
+
"setCode": "0x10"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
## If it still shows as a switch
|
|
161
|
+
- The plugin now explicitly sets the Accessory Category to `TELEVISION`.
|
|
162
|
+
- If the icon remains a box/switch, try restarting Homebridge or removing/re-adding the accessory (or clearing `cachedAccessories`).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
{
|
|
2
|
+
"pluginAlias": "PhilipsDLinePlatform",
|
|
3
|
+
"pluginType": "platform",
|
|
4
|
+
"schema": {
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"displays": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"properties": {
|
|
12
|
+
"name": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"title": "Display name"
|
|
15
|
+
},
|
|
16
|
+
"host": {
|
|
17
|
+
"type": "string",
|
|
18
|
+
"title": "IP / Hostname"
|
|
19
|
+
},
|
|
20
|
+
"port": {
|
|
21
|
+
"type": "number",
|
|
22
|
+
"title": "TCP port",
|
|
23
|
+
"default": 5000
|
|
24
|
+
},
|
|
25
|
+
"monitorId": {
|
|
26
|
+
"type": "number",
|
|
27
|
+
"title": "Monitor ID",
|
|
28
|
+
"default": 1
|
|
29
|
+
},
|
|
30
|
+
"includeGroup": {
|
|
31
|
+
"type": "boolean",
|
|
32
|
+
"title": "Include Group byte",
|
|
33
|
+
"default": true
|
|
34
|
+
},
|
|
35
|
+
"groupId": {
|
|
36
|
+
"type": "number",
|
|
37
|
+
"title": "Group ID",
|
|
38
|
+
"default": 0
|
|
39
|
+
},
|
|
40
|
+
"pollInterval": {
|
|
41
|
+
"type": "number",
|
|
42
|
+
"title": "Poll interval (s)",
|
|
43
|
+
"default": 10
|
|
44
|
+
},
|
|
45
|
+
"exposeInputSwitches": {
|
|
46
|
+
"type": "boolean",
|
|
47
|
+
"title": "Expose per-input switches",
|
|
48
|
+
"default": false
|
|
49
|
+
},
|
|
50
|
+
"exposeBrightness": {
|
|
51
|
+
"type": "boolean",
|
|
52
|
+
"title": "Expose Brightness (as Lightbulb)",
|
|
53
|
+
"description": "WARNING: Exposing brightness creates a Lightbulb service. HomeKit may group this with other lights, causing the TV to turn on when you say 'Turn on all lights'. Disable this if you experience unwanted behavior. The setting provides a slider for backlight control.",
|
|
54
|
+
"default": true
|
|
55
|
+
},
|
|
56
|
+
"inputs": {
|
|
57
|
+
"type": "array",
|
|
58
|
+
"title": "Inputs",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"properties": {
|
|
62
|
+
"label": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"title": "Label"
|
|
65
|
+
},
|
|
66
|
+
"code": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"title": "SICP code (e.g., 0x0D)"
|
|
69
|
+
},
|
|
70
|
+
"identifier": {
|
|
71
|
+
"type": "number",
|
|
72
|
+
"title": "HomeKit Identifier (1..)"
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"required": [
|
|
76
|
+
"label",
|
|
77
|
+
"code",
|
|
78
|
+
"identifier"
|
|
79
|
+
]
|
|
80
|
+
},
|
|
81
|
+
"default": [
|
|
82
|
+
{
|
|
83
|
+
"label": "HDMI 1",
|
|
84
|
+
"code": "0x0D",
|
|
85
|
+
"identifier": 1
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"label": "HDMI 2",
|
|
89
|
+
"code": "0x06",
|
|
90
|
+
"identifier": 2
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"label": "HDMI 3",
|
|
94
|
+
"code": "0x0F",
|
|
95
|
+
"identifier": 3
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"label": "HDMI 4",
|
|
99
|
+
"code": "0x19",
|
|
100
|
+
"identifier": 4
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"required": [
|
|
106
|
+
"name",
|
|
107
|
+
"host"
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
"required": [
|
|
113
|
+
"displays"
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Homebridge plugin to control Philips D-Line signage displays (e.g., 55BDL4511D/00)
|
|
5
|
+
* over LAN using SICP over TCP (default port 5000).
|
|
6
|
+
*
|
|
7
|
+
* Exposes a HomeKit Television with inputs, volume (TelevisionSpeaker), and brightness (as a Lightbulb service).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const net = require('net');
|
|
11
|
+
let hap;
|
|
12
|
+
|
|
13
|
+
const PLUGIN_NAME = 'homebridge-philips-dline-sicp';
|
|
14
|
+
const PLATFORM_NAME = 'PhilipsDLinePlatform';
|
|
15
|
+
|
|
16
|
+
/** Simple promise-based sleep */
|
|
17
|
+
function delay(ms) { return new Promise(res => setTimeout(res, ms)); }
|
|
18
|
+
|
|
19
|
+
/** Clamp helper */
|
|
20
|
+
function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
|
|
21
|
+
|
|
22
|
+
/** A light-weight send queue so we don't overlap TCP writes */
|
|
23
|
+
class SendQueue {
|
|
24
|
+
constructor(sender) {
|
|
25
|
+
this.sender = sender; // async (buf) => Buffer
|
|
26
|
+
this.queue = Promise.resolve();
|
|
27
|
+
}
|
|
28
|
+
send(buf) {
|
|
29
|
+
const job = async () => {
|
|
30
|
+
try {
|
|
31
|
+
return await this.sender(buf);
|
|
32
|
+
} catch (e) {
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const p = this.queue.then(job, job);
|
|
37
|
+
this.queue = p.catch(() => { });
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build a SICP packet */
|
|
43
|
+
function buildSicpPacket(monitorId, dataBytes, includeGroup = true, groupId = 0x00) {
|
|
44
|
+
const body = includeGroup ? [monitorId & 0xFF, groupId & 0xFF, ...dataBytes] : [monitorId & 0xFF, ...dataBytes];
|
|
45
|
+
const msgSize = 1 + body.length + 1; // size + body + checksum
|
|
46
|
+
const arr = [msgSize & 0xFF, ...body];
|
|
47
|
+
let checksum = 0x00;
|
|
48
|
+
for (const b of arr) checksum ^= (b & 0xFF);
|
|
49
|
+
arr.push(checksum & 0xFF);
|
|
50
|
+
return Buffer.from(arr);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Parse a basic ACK/NACK/NAV (best-effort) */
|
|
54
|
+
function parseReply(buf) {
|
|
55
|
+
const bytes = [...buf];
|
|
56
|
+
return {
|
|
57
|
+
raw: bytes.map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '),
|
|
58
|
+
ok: bytes.includes(0x06),
|
|
59
|
+
nack: bytes.includes(0x15),
|
|
60
|
+
nav: bytes.includes(0x18),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** TCP client for SICP */
|
|
65
|
+
class SicpClient {
|
|
66
|
+
constructor(host, port = 5000, timeoutMs = 1200) {
|
|
67
|
+
this.host = host;
|
|
68
|
+
this.port = port;
|
|
69
|
+
this.timeoutMs = timeoutMs;
|
|
70
|
+
this.queue = new SendQueue(this._sendOnce.bind(this));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async send(pkt) {
|
|
74
|
+
return this.queue.send(pkt);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_sendOnce(pkt) {
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const socket = new net.Socket();
|
|
80
|
+
let chunks = [];
|
|
81
|
+
let done = false;
|
|
82
|
+
|
|
83
|
+
const finish = (err, data) => {
|
|
84
|
+
if (done) return;
|
|
85
|
+
done = true;
|
|
86
|
+
try { socket.destroy(); } catch { }
|
|
87
|
+
if (err) reject(err);
|
|
88
|
+
else resolve(Buffer.concat(data || []));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
socket.setTimeout(this.timeoutMs, () => finish(new Error('SICP: timeout'), chunks));
|
|
92
|
+
socket.once('error', (e) => finish(e, chunks));
|
|
93
|
+
socket.connect(this.port, this.host, () => {
|
|
94
|
+
socket.write(pkt);
|
|
95
|
+
});
|
|
96
|
+
socket.on('data', (d) => chunks.push(Buffer.from(d)));
|
|
97
|
+
socket.once('close', () => finish(null, chunks));
|
|
98
|
+
// Some firmwares keep the socket open; close after a short delay
|
|
99
|
+
setTimeout(() => { try { socket.end(); } catch { } }, 200);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Accessory representing one D-Line TV */
|
|
105
|
+
class PhilipsDLineTelevisionAccessory {
|
|
106
|
+
constructor(platform, accessory, conf) {
|
|
107
|
+
this.platform = platform;
|
|
108
|
+
this.accessory = accessory;
|
|
109
|
+
this.log = platform.log;
|
|
110
|
+
this.name = conf.name || 'Philips D-Line';
|
|
111
|
+
this.host = conf.host;
|
|
112
|
+
this.port = conf.port || 5000;
|
|
113
|
+
this.monitorId = (conf.monitorId === 0 || conf.monitorId) ? conf.monitorId : 1;
|
|
114
|
+
this.includeGroup = conf.includeGroup !== false; // default true
|
|
115
|
+
this.groupId = conf.groupId || 0x00;
|
|
116
|
+
this.pollInterval = conf.pollInterval || 10; // seconds
|
|
117
|
+
this.exposeBrightness = conf.exposeBrightness !== false; // default true
|
|
118
|
+
|
|
119
|
+
// Inputs config
|
|
120
|
+
this.inputs = Array.isArray(conf.inputs) && conf.inputs.length ? conf.inputs : [
|
|
121
|
+
{ label: 'HDMI 1', code: '0x0D', identifier: 1 },
|
|
122
|
+
{ label: 'HDMI 2', code: '0x06', identifier: 2 },
|
|
123
|
+
{ label: 'HDMI 3', code: '0x0F', identifier: 3 },
|
|
124
|
+
{ label: 'HDMI 4', code: '0x19', identifier: 4 },
|
|
125
|
+
];
|
|
126
|
+
this.exposeInputSwitches = !!conf.exposeInputSwitches;
|
|
127
|
+
|
|
128
|
+
// Volume config (two modes: absolute set, or relative up/down repeat)
|
|
129
|
+
this.volume = {
|
|
130
|
+
min: conf.volume?.min ?? 0,
|
|
131
|
+
max: conf.volume?.max ?? 100,
|
|
132
|
+
// Absolute set: send [volSetCode, value] if provided
|
|
133
|
+
setCode: conf.volume?.setCode, // e.g. "0x44"
|
|
134
|
+
// Relative: send [volUpCode] or [volDownCode] N times
|
|
135
|
+
upCode: conf.volume?.upCode,
|
|
136
|
+
downCode: conf.volume?.downCode,
|
|
137
|
+
// Mute support
|
|
138
|
+
muteSetCode: conf.volume?.muteSetCode, // absolute mute set [code, 0/1]
|
|
139
|
+
muteToggleCode: conf.volume?.muteToggleCode, // single code to toggle
|
|
140
|
+
// UI state
|
|
141
|
+
current: clamp(conf.volume?.initial ?? 15, conf.volume?.min ?? 0, conf.volume?.max ?? 100),
|
|
142
|
+
muted: false,
|
|
143
|
+
stepMs: conf.volume?.stepDelayMs ?? 120, // delay between relative steps
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Brightness config (absolute preferred; fallback relative)
|
|
147
|
+
this.brightness = {
|
|
148
|
+
min: conf.brightness?.min ?? 0,
|
|
149
|
+
max: conf.brightness?.max ?? 100,
|
|
150
|
+
setCode: conf.brightness?.setCode, // e.g. "0x10"
|
|
151
|
+
upCode: conf.brightness?.upCode,
|
|
152
|
+
downCode: conf.brightness?.downCode,
|
|
153
|
+
current: clamp(conf.brightness?.initial ?? 50, conf.brightness?.min ?? 0, conf.brightness?.max ?? 100),
|
|
154
|
+
stepMs: conf.brightness?.stepDelayMs ?? 120,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// State
|
|
158
|
+
this.active = 0; // 0=INACTIVE, 1=ACTIVE
|
|
159
|
+
this.activeIdentifier = this.inputs[0]?.identifier ?? 1;
|
|
160
|
+
|
|
161
|
+
this.client = new SicpClient(this.host, this.port);
|
|
162
|
+
this._setupServices();
|
|
163
|
+
this._startPolling();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
_setupServices() {
|
|
167
|
+
const Service = hap.Service;
|
|
168
|
+
const Characteristic = hap.Characteristic;
|
|
169
|
+
|
|
170
|
+
this.televisionService = this.accessory.getService(Service.Television)
|
|
171
|
+
|| this.accessory.addService(Service.Television, this.name);
|
|
172
|
+
|
|
173
|
+
this.televisionService
|
|
174
|
+
.setCharacteristic(Characteristic.ConfiguredName, this.name)
|
|
175
|
+
.setCharacteristic(Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE);
|
|
176
|
+
|
|
177
|
+
// Power
|
|
178
|
+
this.televisionService.getCharacteristic(Characteristic.Active)
|
|
179
|
+
.onGet(this.handleGetActive.bind(this))
|
|
180
|
+
.onSet(this.handleSetActive.bind(this));
|
|
181
|
+
|
|
182
|
+
// Active input
|
|
183
|
+
this.televisionService.getCharacteristic(Characteristic.ActiveIdentifier)
|
|
184
|
+
.onGet(async () => this.activeIdentifier)
|
|
185
|
+
.onSet(this.handleSetActiveIdentifier.bind(this));
|
|
186
|
+
|
|
187
|
+
// Add inputs as InputSource services
|
|
188
|
+
this.inputs.forEach((inp, idx) => {
|
|
189
|
+
const id = (typeof inp.identifier === 'number') ? inp.identifier : (idx + 1);
|
|
190
|
+
const label = inp.label || `Input ${id}`;
|
|
191
|
+
|
|
192
|
+
let inputService = this.accessory.getService(label);
|
|
193
|
+
if (!inputService) {
|
|
194
|
+
inputService = this.accessory.addService(Service.InputSource, label, 'input-' + id);
|
|
195
|
+
}
|
|
196
|
+
inputService
|
|
197
|
+
.setCharacteristic(Characteristic.Identifier, id)
|
|
198
|
+
.setCharacteristic(Characteristic.ConfiguredName, label)
|
|
199
|
+
.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI)
|
|
200
|
+
.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED)
|
|
201
|
+
.setCharacteristic(Characteristic.CurrentVisibilityState, Characteristic.CurrentVisibilityState.SHOWN);
|
|
202
|
+
|
|
203
|
+
this.televisionService.addLinkedService(inputService);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Optional: expose input switches
|
|
207
|
+
if (this.exposeInputSwitches) {
|
|
208
|
+
this.inputs.forEach((inp, idx) => {
|
|
209
|
+
const id = (typeof inp.identifier === 'number') ? inp.identifier : (idx + 1);
|
|
210
|
+
const label = `${inp.label || `Input ${id}`} Switch`;
|
|
211
|
+
const s = this.accessory.addService(Service.Switch, label, 'switch-' + id);
|
|
212
|
+
s.getCharacteristic(Characteristic.On)
|
|
213
|
+
.onGet(async () => this.active && (this.activeIdentifier === id))
|
|
214
|
+
.onSet(async (val) => {
|
|
215
|
+
if (val) {
|
|
216
|
+
await this._ensureOn();
|
|
217
|
+
await this._setInputByIdentifier(id);
|
|
218
|
+
// Turn off other input switches
|
|
219
|
+
this.inputs.forEach((other, j) => {
|
|
220
|
+
const oid = (typeof other.identifier === 'number') ? other.identifier : (j + 1);
|
|
221
|
+
if (oid !== id) {
|
|
222
|
+
const os = this.accessory.getService(`${other.label || `Input ${oid}`} Switch`);
|
|
223
|
+
if (os) os.updateCharacteristic(Characteristic.On, false);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
// no-op
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- TelevisionSpeaker (volume & mute) ---
|
|
234
|
+
this.speakerService = this.accessory.getService(Service.TelevisionSpeaker)
|
|
235
|
+
|| this.accessory.addService(Service.TelevisionSpeaker);
|
|
236
|
+
|
|
237
|
+
this.speakerService
|
|
238
|
+
.setCharacteristic(Characteristic.Active, Characteristic.Active.ACTIVE)
|
|
239
|
+
.setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE); // we emulate absolute
|
|
240
|
+
|
|
241
|
+
this.speakerService.getCharacteristic(Characteristic.Mute)
|
|
242
|
+
.onGet(async () => this.volume.muted)
|
|
243
|
+
.onSet(this.handleSetMute.bind(this));
|
|
244
|
+
|
|
245
|
+
this.speakerService.getCharacteristic(Characteristic.Volume)
|
|
246
|
+
.onGet(async () => this.volume.current)
|
|
247
|
+
.onSet(this.handleSetVolume.bind(this));
|
|
248
|
+
|
|
249
|
+
// Link speaker to TV
|
|
250
|
+
this.televisionService.addLinkedService(this.speakerService);
|
|
251
|
+
|
|
252
|
+
// --- Brightness as a Lightbulb service ---
|
|
253
|
+
if (this.exposeBrightness) {
|
|
254
|
+
this.backlightService = this.accessory.getService('Backlight')
|
|
255
|
+
|| this.accessory.addService(Service.Lightbulb, 'Backlight', 'backlight');
|
|
256
|
+
|
|
257
|
+
this.backlightService.getCharacteristic(Characteristic.On)
|
|
258
|
+
.onGet(async () => this.active === 1 && this.brightness.current > this.brightness.min)
|
|
259
|
+
.onSet(async (val) => {
|
|
260
|
+
if (val) {
|
|
261
|
+
// If TV is OFF, do NOT turn it on automatically just because "All lights" were turned on.
|
|
262
|
+
if (this.active !== 1) {
|
|
263
|
+
this.log.info('Ignoring Backlight On request because TV is OFF.');
|
|
264
|
+
// We can throw an error to indicate failure, or just revert the state.
|
|
265
|
+
// Throwing an error might be annoying in scenes, but reverting is cleaner.
|
|
266
|
+
setTimeout(() => {
|
|
267
|
+
this.backlightService.updateCharacteristic(Characteristic.On, false);
|
|
268
|
+
}, 100);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (this.brightness.current <= this.brightness.min) {
|
|
272
|
+
await this._setBrightness(Math.max(this.brightness.min + 1, 10));
|
|
273
|
+
}
|
|
274
|
+
} else {
|
|
275
|
+
// Turning off backlight -> set brightness to min
|
|
276
|
+
await this._setBrightness(this.brightness.min);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
this.backlightService.getCharacteristic(Characteristic.Brightness)
|
|
281
|
+
.onGet(async () => this.brightness.current)
|
|
282
|
+
.onSet(async (val) => {
|
|
283
|
+
// If TV is OFF, ignore brightness set
|
|
284
|
+
if (this.active !== 1) {
|
|
285
|
+
this.log.info('Ignoring Brightness Set request because TV is OFF.');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
await this._setBrightness(val);
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
// Remove service if it exists (e.g. if user disabled it)
|
|
292
|
+
const existing = this.accessory.getService('Backlight');
|
|
293
|
+
if (existing) {
|
|
294
|
+
this.accessory.removeService(existing);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Publish Accessory information
|
|
299
|
+
const info = this.accessory.getService(Service.AccessoryInformation);
|
|
300
|
+
info
|
|
301
|
+
.setCharacteristic(Characteristic.Manufacturer, 'Philips (Signage)')
|
|
302
|
+
.setCharacteristic(Characteristic.Model, 'D-Line (SICP over IP)')
|
|
303
|
+
.setCharacteristic(Characteristic.SerialNumber, this.host);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ---------------- Handlers ----------------
|
|
307
|
+
|
|
308
|
+
async handleGetActive() {
|
|
309
|
+
try {
|
|
310
|
+
const pkt = buildSicpPacket(this.monitorId, [0x19], this.includeGroup, this.groupId); // Get Power
|
|
311
|
+
const reply = await this.client.send(pkt);
|
|
312
|
+
const parsed = parseReply(reply);
|
|
313
|
+
// parsed.raw is e.g. "0x06 0x02" where 0x02 is ON.
|
|
314
|
+
this.platform.log.debug('GetActive reply:', parsed.raw);
|
|
315
|
+
|
|
316
|
+
// SICP Get Power Reply: [Len, Mon, Grp, ACK(06), Status] or just [ACK, Status] depending on model.
|
|
317
|
+
// We need to actually parse the status byte.
|
|
318
|
+
// Assuming the last byte before checksum or the byte after 0x06 is status.
|
|
319
|
+
// Let's look for 0x02 (On) or 0x01 (Standby) in the reply.
|
|
320
|
+
const bytes = [...reply];
|
|
321
|
+
// Simple heuristic: if it contains 0x02, it's ON. If 0x01, it's OFF.
|
|
322
|
+
// Note: This matches the Set Power command (0x02=On, 0x01=Off).
|
|
323
|
+
if (bytes.includes(0x02)) {
|
|
324
|
+
this.active = 1;
|
|
325
|
+
} else if (bytes.includes(0x01)) {
|
|
326
|
+
this.active = 0;
|
|
327
|
+
}
|
|
328
|
+
// If we got a reply but couldn't check status, we validly connected at least.
|
|
329
|
+
// But typically we should trust the parsed status.
|
|
330
|
+
|
|
331
|
+
return this.active;
|
|
332
|
+
} catch (e) {
|
|
333
|
+
this.log.warn('GetActive failed (TV unreachable?):', e.message);
|
|
334
|
+
this.active = 0; // Assume OFF if unreachable
|
|
335
|
+
return this.active;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async handleSetActive(value) {
|
|
340
|
+
const on = (value === 1 || value === true);
|
|
341
|
+
try {
|
|
342
|
+
const pkt = buildSicpPacket(this.monitorId, [0x18, on ? 0x02 : 0x01], this.includeGroup, this.groupId); // Set Power
|
|
343
|
+
const reply = await this.client.send(pkt);
|
|
344
|
+
const parsed = parseReply(reply);
|
|
345
|
+
this.log.debug('SetActive reply:', parsed.raw);
|
|
346
|
+
if (parsed.nack || parsed.nav) throw new Error('Device rejected command');
|
|
347
|
+
this.active = on ? 1 : 0;
|
|
348
|
+
if (!on && this.exposeInputSwitches) {
|
|
349
|
+
this.inputs.forEach((inp, idx) => {
|
|
350
|
+
const id = (typeof inp.identifier === 'number') ? inp.identifier : (idx + 1);
|
|
351
|
+
const s = this.accessory.getService(`${inp.label || `Input ${id}`} Switch`);
|
|
352
|
+
if (s) s.updateCharacteristic(hap.Characteristic.On, false);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
} catch (e) {
|
|
356
|
+
this.log.error('SetActive error:', e.message);
|
|
357
|
+
this.televisionService.updateCharacteristic(hap.Characteristic.Active, this.active);
|
|
358
|
+
throw e;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
async handleSetActiveIdentifier(identifier) {
|
|
363
|
+
await this._ensureOn();
|
|
364
|
+
await this._setInputByIdentifier(identifier);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async handleSetVolume(val) {
|
|
368
|
+
const target = clamp(Number(val), this.volume.min, this.volume.max);
|
|
369
|
+
await this._ensureOn();
|
|
370
|
+
if (this.volume.setCode) {
|
|
371
|
+
// Absolute mode
|
|
372
|
+
const code = this._parseCode(this.volume.setCode);
|
|
373
|
+
if (code === 0x44) {
|
|
374
|
+
// SICP Volume Set: [0x44, SpeakerVol, AudioOutVol]; use 0xFF (no change) for Audio Out
|
|
375
|
+
await this._send([code, target & 0xFF, 0xFF]);
|
|
376
|
+
} else {
|
|
377
|
+
await this._send([code, target & 0xFF]);
|
|
378
|
+
}
|
|
379
|
+
} else if (this.volume.upCode && this.volume.downCode) {
|
|
380
|
+
// Relative mode: step towards target
|
|
381
|
+
const diff = target - this.volume.current;
|
|
382
|
+
const stepCode = diff > 0 ? this._parseCode(this.volume.upCode) : this._parseCode(this.volume.downCode);
|
|
383
|
+
for (let i = 0; i < Math.abs(diff); i++) {
|
|
384
|
+
await this._send([stepCode]);
|
|
385
|
+
await delay(this.volume.stepMs);
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
this.log.warn('Volume codes not configured; ignoring setVolume.');
|
|
389
|
+
}
|
|
390
|
+
this.volume.current = target;
|
|
391
|
+
this.speakerService.updateCharacteristic(hap.Characteristic.Volume, this.volume.current);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async handleSetMute(val) {
|
|
395
|
+
const mute = !!val;
|
|
396
|
+
await this._ensureOn();
|
|
397
|
+
if (this.volume.muteSetCode) {
|
|
398
|
+
const code = this._parseCode(this.volume.muteSetCode);
|
|
399
|
+
await this._send([code, mute ? 0x01 : 0x00]);
|
|
400
|
+
} else if (this.volume.muteToggleCode) {
|
|
401
|
+
const code = this._parseCode(this.volume.muteToggleCode);
|
|
402
|
+
// If desired state differs, send one toggle
|
|
403
|
+
if (mute !== this.volume.muted) {
|
|
404
|
+
await this._send([code]);
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
this.log.warn('Mute codes not configured; ignoring mute.');
|
|
408
|
+
}
|
|
409
|
+
this.volume.muted = mute;
|
|
410
|
+
this.speakerService.updateCharacteristic(hap.Characteristic.Mute, this.volume.muted);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ---------------- Helpers ----------------
|
|
414
|
+
|
|
415
|
+
async _ensureOn() {
|
|
416
|
+
if (this.active !== 1) {
|
|
417
|
+
await this.handleSetActive(1);
|
|
418
|
+
await delay(300);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
_codeFromIdentifier(identifier) {
|
|
423
|
+
const inp = this.inputs.find(x => (x.identifier === identifier));
|
|
424
|
+
if (!inp) return null;
|
|
425
|
+
const raw = (typeof inp.code === 'string') ? inp.code : inp.code?.toString();
|
|
426
|
+
if (!raw) return null;
|
|
427
|
+
const code = raw.trim().toLowerCase().startsWith('0x') ? parseInt(raw, 16) : parseInt(raw, 10);
|
|
428
|
+
if (Number.isNaN(code)) return null;
|
|
429
|
+
return code & 0xFF;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
_parseCode(raw) {
|
|
433
|
+
if (typeof raw === 'number') return raw & 0xFF;
|
|
434
|
+
const s = String(raw).trim().toLowerCase();
|
|
435
|
+
return (s.startsWith('0x') ? parseInt(s, 16) : parseInt(s, 10)) & 0xFF;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async _setInputByIdentifier(identifier) {
|
|
439
|
+
const code = this._codeFromIdentifier(identifier);
|
|
440
|
+
if (code == null) {
|
|
441
|
+
this.log.error('Unknown input identifier:', identifier);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const reply = await this._send([0xAC, code]); // Set Input
|
|
445
|
+
const parsed = parseReply(reply);
|
|
446
|
+
this.log.debug(`SetInput(${identifier}/0x${code.toString(16)}) reply:`, parsed.raw);
|
|
447
|
+
if (parsed.nack || parsed.nav) throw new Error('Device rejected input change');
|
|
448
|
+
this.activeIdentifier = identifier;
|
|
449
|
+
this.televisionService.updateCharacteristic(hap.Characteristic.ActiveIdentifier, identifier);
|
|
450
|
+
if (this.exposeInputSwitches) {
|
|
451
|
+
this.inputs.forEach((inp, idx) => {
|
|
452
|
+
const id = (typeof inp.identifier === 'number') ? inp.identifier : (idx + 1);
|
|
453
|
+
const s = this.accessory.getService(`${inp.label || `Input ${id}`} Switch`);
|
|
454
|
+
if (s) s.updateCharacteristic(hap.Characteristic.On, id === identifier);
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async _setBrightness(val) {
|
|
460
|
+
const target = clamp(Number(val), this.brightness.min, this.brightness.max);
|
|
461
|
+
if (this.brightness.setCode) {
|
|
462
|
+
const code = this._parseCode(this.brightness.setCode);
|
|
463
|
+
if (code === 0x32) {
|
|
464
|
+
// SICP Video Parameters Set: [0x32, Brightness, Color, Contrast, Sharpness, Tint, BlackLevel, Gamma]
|
|
465
|
+
// Use 0xFF for "no change" on other fields (supported since SICP 2.09)
|
|
466
|
+
await this._send([code, target & 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
|
|
467
|
+
} else {
|
|
468
|
+
await this._send([code, target & 0xFF]);
|
|
469
|
+
}
|
|
470
|
+
} else if (this.brightness.upCode && this.brightness.downCode) {
|
|
471
|
+
const diff = target - this.brightness.current;
|
|
472
|
+
const stepCode = diff > 0 ? this._parseCode(this.brightness.upCode) : this._parseCode(this.brightness.downCode);
|
|
473
|
+
for (let i = 0; i < Math.abs(diff); i++) {
|
|
474
|
+
await this._send([stepCode]);
|
|
475
|
+
await delay(this.brightness.stepMs);
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
// Fallback: If no brightness setCode or up/down codes, try default 0x32 (Video Parameters)
|
|
479
|
+
// Many D-Lines support 0x32 for brightness.
|
|
480
|
+
this.log.debug('No brightness codes configured, attempting default SICP 0x32 command.');
|
|
481
|
+
try {
|
|
482
|
+
// [0x32, Brightness, Color, Contrast, Sharpness, Tint, BlackLevel, Gamma]
|
|
483
|
+
// We'll try just sending [0x32, val]. Some older SICP might accept just valid params.
|
|
484
|
+
// Or better, use the full safe string:
|
|
485
|
+
// await this._send([0x32, target & 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
|
|
486
|
+
// Wait, the code existed but was inside 'if (this.brightness.setCode)'.
|
|
487
|
+
// Let's force try 0x32 if nothing else is set.
|
|
488
|
+
await this._send([0x32, target & 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF]);
|
|
489
|
+
} catch (e) {
|
|
490
|
+
this.log.warn('Default brightness 0x32 failed:', e.message);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
this.brightness.current = target;
|
|
494
|
+
if (this.exposeBrightness) {
|
|
495
|
+
this.backlightService.updateCharacteristic(hap.Characteristic.Brightness, this.brightness.current);
|
|
496
|
+
this.backlightService.updateCharacteristic(hap.Characteristic.On, this.brightness.current > this.brightness.min);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async _send(dataBytes) {
|
|
501
|
+
const pkt = buildSicpPacket(this.monitorId, dataBytes, this.includeGroup, this.groupId);
|
|
502
|
+
const reply = await this.client.send(pkt);
|
|
503
|
+
return reply;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
_startPolling() {
|
|
507
|
+
if (!this.pollInterval || this.pollInterval <= 0) return;
|
|
508
|
+
const loop = async () => {
|
|
509
|
+
try {
|
|
510
|
+
await delay(this.pollInterval * 1000);
|
|
511
|
+
await this.handleGetActive().catch(() => { });
|
|
512
|
+
} catch (e) {
|
|
513
|
+
// ignore
|
|
514
|
+
} finally {
|
|
515
|
+
setImmediate(loop);
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
loop();
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Platform (supports multiple displays) */
|
|
523
|
+
class PhilipsDLinePlatform {
|
|
524
|
+
constructor(log, config, api) {
|
|
525
|
+
this.log = log;
|
|
526
|
+
this.config = config || {};
|
|
527
|
+
this.api = api;
|
|
528
|
+
hap = api.hap;
|
|
529
|
+
|
|
530
|
+
this.accessories = new Map(); // UUID -> accessory
|
|
531
|
+
|
|
532
|
+
if (!this.config.displays || !Array.isArray(this.config.displays) || this.config.displays.length === 0) {
|
|
533
|
+
this.log.warn('No "displays" configured. Please add at least one display.');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
api.on('didFinishLaunching', () => {
|
|
537
|
+
this.discover();
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
configureAccessory(accessory) {
|
|
542
|
+
this.accessories.set(accessory.UUID, accessory);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
discover() {
|
|
546
|
+
const displays = this.config.displays || [];
|
|
547
|
+
displays.forEach(conf => {
|
|
548
|
+
if (!conf || !conf.host) {
|
|
549
|
+
this.log.warn('Skipping display without "host" field:', conf);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const uuid = this.api.hap.uuid.generate(`philips-dline:${conf.host}:${conf.name || ''}`);
|
|
553
|
+
let accessory = this.accessories.get(uuid);
|
|
554
|
+
if (!accessory) {
|
|
555
|
+
accessory = new this.api.platformAccessory(conf.name || 'Philips D-Line', uuid);
|
|
556
|
+
accessory.context.conf = conf;
|
|
557
|
+
// Fix for icon issue: Set category to TELEVISION
|
|
558
|
+
accessory.category = this.api.hap.Categories.TELEVISION;
|
|
559
|
+
|
|
560
|
+
this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]);
|
|
561
|
+
this.accessories.set(uuid, accessory);
|
|
562
|
+
this.log.info('Registered new display accessory:', conf.name || conf.host);
|
|
563
|
+
} else {
|
|
564
|
+
accessory.context.conf = conf;
|
|
565
|
+
// Ensure category is updated if it was missing
|
|
566
|
+
accessory.category = this.api.hap.Categories.TELEVISION;
|
|
567
|
+
this.log.info('Updated display accessory:', conf.name || conf.host);
|
|
568
|
+
}
|
|
569
|
+
new PhilipsDLineTelevisionAccessory(this, accessory, conf);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
module.exports = (api) => {
|
|
575
|
+
api.registerPlatform(PLATFORM_NAME, PhilipsDLinePlatform);
|
|
576
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "homebridge-philips-dline-sicp",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Homebridge plugin to control Philips D-Line signage displays (e.g., 55BDL4511D) over LAN using SICP (TCP:5000). Exposes as a HomeKit Television with inputs.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"homebridge-plugin",
|
|
8
|
+
"philips",
|
|
9
|
+
"d-line",
|
|
10
|
+
"signage",
|
|
11
|
+
"television",
|
|
12
|
+
"sicp"
|
|
13
|
+
],
|
|
14
|
+
"author": "ChatGPT",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0",
|
|
18
|
+
"homebridge": ">=1.6.0"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {},
|
|
21
|
+
"devDependencies": {},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://example.com/homebridge-philips-dline-sicp.git"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/yourname/homebridge-philips-dline-sicp",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/yourname/homebridge-philips-dline-sicp/issues"
|
|
29
|
+
},
|
|
30
|
+
"contributors": [
|
|
31
|
+
"Thomas Dazy"
|
|
32
|
+
],
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
}
|
|
36
|
+
}
|