homebridge-hap-homematic 0.4.1 → 0.5.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 +1 -4
- package/config.schema.json +0 -1
- package/homebridge-ui/public/app.js +47 -13
- package/homebridge-ui/server.js +37 -9
- package/homebridge-ui/server.src.js +43 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
# homebridge-hap-homematic
|
|
2
2
|
|
|
3
|
-
<!--
|
|
4
|
-
Modified by california444 (2026) for homebridge-hap-homematic,
|
|
5
|
-
a hard fork of Homebridge-Homematic-HAP (Apache-2.0). See NOTICE.
|
|
6
|
-
-->
|
|
3
|
+
<!-- Modified by california444 (2026). See NOTICE. -->
|
|
7
4
|
|
|
8
5
|
[](https://github.com/california444/homebridge-hap-homematic/actions/workflows/release.yml)
|
|
9
6
|
[](LICENSE)
|
package/config.schema.json
CHANGED
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
"customUi": true,
|
|
7
7
|
"customUiPath": "./homebridge-ui",
|
|
8
8
|
"headerDisplay": "**HomeMatic (BidCos-RF) / HomematicIP (HmIP-RF)** plugin for a CCU / OpenCCU, using XML-RPC only. Use the **custom UI** below to discover devices and map channels to HomeKit.",
|
|
9
|
-
"footerDisplay": "Hard fork of [Homebridge-Homematic-HAP](https://github.com/matthiaseinig/Homebridge-Homematic-HAP), which builds on [thkl/hap-homematic](https://github.com/thkl/hap-homematic) and [thkl/homebridge-homematic](https://github.com/thkl/homebridge-homematic). Apache-2.0.",
|
|
10
9
|
"schema": {
|
|
11
10
|
"type": "object",
|
|
12
11
|
"properties": {
|
|
@@ -316,7 +316,7 @@ function viewChannels(host) {
|
|
|
316
316
|
host.innerHTML = `
|
|
317
317
|
<div class="hgui-stats mt-3">
|
|
318
318
|
${statHTML('Channels', configuredChannels().length, 'in HomeKit')}
|
|
319
|
-
${statHTML('Devices', discovered,
|
|
319
|
+
${statHTML('Devices', discovered, discoverySourceText())}
|
|
320
320
|
<div class="hgui-card d-flex flex-column justify-content-center gap-1">
|
|
321
321
|
<button class="btn btn-primary" id="hgui-discover-btn">Discover</button>
|
|
322
322
|
<div class="hgui-meta">Reload devices and channels from the CCU.</div>
|
|
@@ -369,7 +369,7 @@ function viewChannels(host) {
|
|
|
369
369
|
$('hgui-filterType-channels').value = channelTypes.has(state.ui.filterType.channels) ? state.ui.filterType.channels : 'all';
|
|
370
370
|
$('hgui-sort-channels').value = state.ui.sort.channels;
|
|
371
371
|
|
|
372
|
-
$('hgui-discover-btn').addEventListener('click', onDiscover);
|
|
372
|
+
$('hgui-discover-btn').addEventListener('click', () => onDiscover());
|
|
373
373
|
$('hgui-help').addEventListener('toggle', (e) => { state.ui.helpOpen = e.target.open; });
|
|
374
374
|
|
|
375
375
|
$('hgui-search-channels').addEventListener('input', (e) => {
|
|
@@ -1009,18 +1009,50 @@ async function onTestConnection() {
|
|
|
1009
1009
|
} finally { spinner(false); }
|
|
1010
1010
|
}
|
|
1011
1011
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1012
|
+
/**
|
|
1013
|
+
* Live discovery from the CCU (listDevices, no radio traffic).
|
|
1014
|
+
* `quiet`: no spinner and no success toast (automatic fallback on open);
|
|
1015
|
+
* errors are always shown.
|
|
1016
|
+
*/
|
|
1017
|
+
async function onDiscover({ quiet = false } = {}) {
|
|
1018
|
+
if (!quiet) spinner(true);
|
|
1014
1019
|
try {
|
|
1015
1020
|
const res = await homebridge.request('/discover', state.cfg);
|
|
1016
1021
|
state.discovered = res;
|
|
1017
1022
|
rerender();
|
|
1018
|
-
toast('success',
|
|
1019
|
-
`${res.devices.length} devices`,
|
|
1020
|
-
'Discovery complete');
|
|
1023
|
+
if (!quiet) toast('success', `${res.devices.length} devices`, 'Discovery complete');
|
|
1021
1024
|
} catch (err) {
|
|
1022
1025
|
toast('error', err.message, 'Discovery');
|
|
1023
|
-
} finally { spinner(false); }
|
|
1026
|
+
} finally { if (!quiet) spinner(false); }
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* The device list the plugin saved at its last start. Returns false if
|
|
1031
|
+
* there is none, so the caller can fall back to a live discovery.
|
|
1032
|
+
*/
|
|
1033
|
+
async function loadCachedDiscovery() {
|
|
1034
|
+
try {
|
|
1035
|
+
const res = await homebridge.request('/discover/cached');
|
|
1036
|
+
if (!res?.devices?.length) return false;
|
|
1037
|
+
state.discovered = res;
|
|
1038
|
+
rerender();
|
|
1039
|
+
return true;
|
|
1040
|
+
} catch {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/** "live, 22:05" / "saved by the plugin, 12.11. 22:05" for the Devices box. */
|
|
1046
|
+
function discoverySourceText() {
|
|
1047
|
+
const d = state.discovered;
|
|
1048
|
+
if (!d.devices?.length) return 'not discovered yet';
|
|
1049
|
+
const at = d.fetchedAt ? new Date(d.fetchedAt) : null;
|
|
1050
|
+
const when = at && !Number.isNaN(at.getTime())
|
|
1051
|
+
? at.toLocaleString(undefined, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
|
1052
|
+
: '';
|
|
1053
|
+
return d.source === 'cache'
|
|
1054
|
+
? `saved by the plugin${when ? `, ${when}` : ''}`
|
|
1055
|
+
: `live from the CCU${when ? `, ${when}` : ''}`;
|
|
1024
1056
|
}
|
|
1025
1057
|
|
|
1026
1058
|
/**
|
|
@@ -1098,16 +1130,18 @@ async function init() {
|
|
|
1098
1130
|
if (e.key === 'Escape' && state.nav.length) popNav();
|
|
1099
1131
|
});
|
|
1100
1132
|
|
|
1101
|
-
// Initial tab: Settings until a CCU is configured, then Channels
|
|
1102
|
-
//
|
|
1133
|
+
// Initial tab: Settings until a CCU is configured, then Channels. The
|
|
1134
|
+
// device list comes from the plugin's discovery.json (no CCU call);
|
|
1135
|
+
// only if there is none (plugin never started) a quiet live discovery
|
|
1136
|
+
// runs. The Discover button always asks the CCU.
|
|
1103
1137
|
const isConfigured = !!state.cfg.ccuIp && state.cfg.ccuIp.length > 0;
|
|
1104
1138
|
if (!isConfigured) {
|
|
1105
1139
|
navigate('settings');
|
|
1106
1140
|
} else {
|
|
1107
1141
|
navigate('channels');
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1142
|
+
loadCachedDiscovery()
|
|
1143
|
+
.then((ok) => (ok ? undefined : onDiscover({ quiet: true })))
|
|
1144
|
+
.catch(() => { /* errors already toasted by onDiscover */ });
|
|
1111
1145
|
}
|
|
1112
1146
|
}
|
|
1113
1147
|
|
package/homebridge-ui/server.js
CHANGED
|
@@ -148,7 +148,8 @@ var RequestError = class _RequestError extends Error {
|
|
|
148
148
|
};
|
|
149
149
|
|
|
150
150
|
// homebridge-ui/server.src.js
|
|
151
|
-
import { CcuClient } from "../dist/src/ccu/CcuClient.js";
|
|
151
|
+
import { CcuClient, toDevices } from "../dist/src/ccu/CcuClient.js";
|
|
152
|
+
import { DISCOVERY_CACHE_FILE } from "../dist/src/ccu/Discovery.js";
|
|
152
153
|
import { resolveConfig, ConfigError } from "../dist/src/util/config.js";
|
|
153
154
|
import { PrefixedLogger } from "../dist/src/util/logger.js";
|
|
154
155
|
import {
|
|
@@ -183,6 +184,15 @@ var PLUGIN_VERSION = (() => {
|
|
|
183
184
|
var MAX_DEVICES_JSON_BYTES = 512 * 1024;
|
|
184
185
|
var KNOWN_SERVICES = new Set(SERVICE_DEFINITIONS.map((s) => s.key));
|
|
185
186
|
var DEVICE_SERVICES = new Set(SERVICE_DEFINITIONS.filter((s) => s.scope === "device").map((s) => s.key));
|
|
187
|
+
function enrich(devices) {
|
|
188
|
+
return devices.map((d) => ({
|
|
189
|
+
...d,
|
|
190
|
+
channels: d.channels.map((c) => ({
|
|
191
|
+
...c,
|
|
192
|
+
suggestedServices: servicesForChannelType(c.type).map((s) => s.key)
|
|
193
|
+
}))
|
|
194
|
+
}));
|
|
195
|
+
}
|
|
186
196
|
var UiLogger = class {
|
|
187
197
|
info(...args) {
|
|
188
198
|
console.log("[ui]", ...args);
|
|
@@ -209,6 +219,7 @@ var HapHomematicUiServer = class extends HomebridgePluginUiServer {
|
|
|
209
219
|
this.onRequest("/services", () => this.handleServices());
|
|
210
220
|
this.onRequest("/test-connection", (payload) => this.handleTestConnection(payload));
|
|
211
221
|
this.onRequest("/discover", (payload) => this.handleDiscover(payload));
|
|
222
|
+
this.onRequest("/discover/cached", () => this.handleCachedDiscovery());
|
|
212
223
|
this.onRequest("/network", () => this.handleNetwork());
|
|
213
224
|
this.onRequest("/devices/get", () => this.handleGetDevices());
|
|
214
225
|
this.onRequest("/devices/put", (payload) => this.handlePutDevices(payload));
|
|
@@ -259,14 +270,7 @@ var HapHomematicUiServer = class extends HomebridgePluginUiServer {
|
|
|
259
270
|
const ccu = new CcuClient({ config, log });
|
|
260
271
|
try {
|
|
261
272
|
const devices = await ccu.listDevices();
|
|
262
|
-
|
|
263
|
-
...d,
|
|
264
|
-
channels: d.channels.map((c) => ({
|
|
265
|
-
...c,
|
|
266
|
-
suggestedServices: servicesForChannelType(c.type).map((s) => s.key)
|
|
267
|
-
}))
|
|
268
|
-
}));
|
|
269
|
-
return { devices: enrichedDevices };
|
|
273
|
+
return { devices: enrich(devices), source: "live", fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
270
274
|
} catch (err) {
|
|
271
275
|
throw new RequestError(`Discovery failed: ${err.message}`, { status: 502 });
|
|
272
276
|
} finally {
|
|
@@ -276,6 +280,30 @@ var HapHomematicUiServer = class extends HomebridgePluginUiServer {
|
|
|
276
280
|
}
|
|
277
281
|
}
|
|
278
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* The device list the plugin saved at its last start (discovery.json),
|
|
285
|
+
* without asking the CCU. `devices: []` if there is none (plugin never
|
|
286
|
+
* started) or it is unreadable; the UI then discovers live.
|
|
287
|
+
*/
|
|
288
|
+
async handleCachedDiscovery() {
|
|
289
|
+
let raw;
|
|
290
|
+
try {
|
|
291
|
+
raw = await this.storage().readJson(DISCOVERY_CACHE_FILE);
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.warn("[ui] cannot read %s: %s", DISCOVERY_CACHE_FILE, err.message);
|
|
294
|
+
return { devices: [], source: "cache" };
|
|
295
|
+
}
|
|
296
|
+
const interfaces = raw && typeof raw === "object" && raw.version === 1 && raw.interfaces && typeof raw.interfaces === "object" ? raw.interfaces : {};
|
|
297
|
+
const devices = [];
|
|
298
|
+
const stamps = [];
|
|
299
|
+
for (const [id, entry] of Object.entries(interfaces)) {
|
|
300
|
+
if (!entry || !Array.isArray(entry.devices)) continue;
|
|
301
|
+
devices.push(...toDevices(entry.devices, id));
|
|
302
|
+
if (typeof entry.fetchedAt === "string") stamps.push(entry.fetchedAt);
|
|
303
|
+
}
|
|
304
|
+
stamps.sort();
|
|
305
|
+
return { devices: enrich(devices), source: "cache", fetchedAt: stamps[0] };
|
|
306
|
+
}
|
|
279
307
|
/** Local IPv4 addresses, offered as callback address (5.1). */
|
|
280
308
|
handleNetwork() {
|
|
281
309
|
const out = [];
|
|
@@ -46,7 +46,8 @@ const PLUGIN_VERSION = (() => {
|
|
|
46
46
|
return version;
|
|
47
47
|
})();
|
|
48
48
|
|
|
49
|
-
import { CcuClient } from '../dist/src/ccu/CcuClient.js';
|
|
49
|
+
import { CcuClient, toDevices } from '../dist/src/ccu/CcuClient.js';
|
|
50
|
+
import { DISCOVERY_CACHE_FILE } from '../dist/src/ccu/Discovery.js';
|
|
50
51
|
import { resolveConfig, ConfigError } from '../dist/src/util/config.js';
|
|
51
52
|
import { PrefixedLogger } from '../dist/src/util/logger.js';
|
|
52
53
|
import {
|
|
@@ -66,6 +67,17 @@ const MAX_DEVICES_JSON_BYTES = 512 * 1024;
|
|
|
66
67
|
const KNOWN_SERVICES = new Set(SERVICE_DEFINITIONS.map((s) => s.key));
|
|
67
68
|
const DEVICE_SERVICES = new Set(SERVICE_DEFINITIONS.filter((s) => s.scope === 'device').map((s) => s.key));
|
|
68
69
|
|
|
70
|
+
/** Annotate channels with the services that can handle them. */
|
|
71
|
+
function enrich(devices) {
|
|
72
|
+
return devices.map((d) => ({
|
|
73
|
+
...d,
|
|
74
|
+
channels: d.channels.map((c) => ({
|
|
75
|
+
...c,
|
|
76
|
+
suggestedServices: servicesForChannelType(c.type).map((s) => s.key),
|
|
77
|
+
})),
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
class UiLogger {
|
|
70
82
|
info(...args) { console.log('[ui]', ...args); }
|
|
71
83
|
success(...args) { console.log('[ui]', ...args); }
|
|
@@ -83,6 +95,7 @@ class HapHomematicUiServer extends HomebridgePluginUiServer {
|
|
|
83
95
|
this.onRequest('/services', () => this.handleServices());
|
|
84
96
|
this.onRequest('/test-connection', (payload) => this.handleTestConnection(payload));
|
|
85
97
|
this.onRequest('/discover', (payload) => this.handleDiscover(payload));
|
|
98
|
+
this.onRequest('/discover/cached', () => this.handleCachedDiscovery());
|
|
86
99
|
this.onRequest('/network', () => this.handleNetwork());
|
|
87
100
|
this.onRequest('/devices/get', () => this.handleGetDevices());
|
|
88
101
|
this.onRequest('/devices/put', (payload) => this.handlePutDevices(payload));
|
|
@@ -139,17 +152,7 @@ class HapHomematicUiServer extends HomebridgePluginUiServer {
|
|
|
139
152
|
const ccu = new CcuClient({ config, log });
|
|
140
153
|
try {
|
|
141
154
|
const devices = await ccu.listDevices();
|
|
142
|
-
|
|
143
|
-
// Annotate channels with suggested services.
|
|
144
|
-
const enrichedDevices = devices.map((d) => ({
|
|
145
|
-
...d,
|
|
146
|
-
channels: d.channels.map((c) => ({
|
|
147
|
-
...c,
|
|
148
|
-
suggestedServices: servicesForChannelType(c.type).map((s) => s.key),
|
|
149
|
-
})),
|
|
150
|
-
}));
|
|
151
|
-
|
|
152
|
-
return { devices: enrichedDevices };
|
|
155
|
+
return { devices: enrich(devices), source: 'live', fetchedAt: new Date().toISOString() };
|
|
153
156
|
} catch (err) {
|
|
154
157
|
throw new RequestError(`Discovery failed: ${err.message}`, { status: 502 });
|
|
155
158
|
} finally {
|
|
@@ -157,6 +160,34 @@ class HapHomematicUiServer extends HomebridgePluginUiServer {
|
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
162
|
|
|
163
|
+
/**
|
|
164
|
+
* The device list the plugin saved at its last start (discovery.json),
|
|
165
|
+
* without asking the CCU. `devices: []` if there is none (plugin never
|
|
166
|
+
* started) or it is unreadable; the UI then discovers live.
|
|
167
|
+
*/
|
|
168
|
+
async handleCachedDiscovery() {
|
|
169
|
+
let raw;
|
|
170
|
+
try {
|
|
171
|
+
raw = await this.storage().readJson(DISCOVERY_CACHE_FILE);
|
|
172
|
+
} catch (err) {
|
|
173
|
+
console.warn('[ui] cannot read %s: %s', DISCOVERY_CACHE_FILE, err.message);
|
|
174
|
+
return { devices: [], source: 'cache' };
|
|
175
|
+
}
|
|
176
|
+
const interfaces = raw && typeof raw === 'object' && raw.version === 1 && raw.interfaces && typeof raw.interfaces === 'object'
|
|
177
|
+
? raw.interfaces
|
|
178
|
+
: {};
|
|
179
|
+
const devices = [];
|
|
180
|
+
const stamps = [];
|
|
181
|
+
for (const [id, entry] of Object.entries(interfaces)) {
|
|
182
|
+
if (!entry || !Array.isArray(entry.devices)) continue;
|
|
183
|
+
devices.push(...toDevices(entry.devices, id));
|
|
184
|
+
if (typeof entry.fetchedAt === 'string') stamps.push(entry.fetchedAt);
|
|
185
|
+
}
|
|
186
|
+
// The oldest interface decides how fresh the list is.
|
|
187
|
+
stamps.sort();
|
|
188
|
+
return { devices: enrich(devices), source: 'cache', fetchedAt: stamps[0] };
|
|
189
|
+
}
|
|
190
|
+
|
|
160
191
|
/** Local IPv4 addresses, offered as callback address (5.1). */
|
|
161
192
|
handleNetwork() {
|
|
162
193
|
const out = [];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-hap-homematic",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Homebridge plugin for HomeMatic / HomematicIP devices on a CCU / OpenCCU, using XML-RPC only.
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Homebridge plugin for HomeMatic / HomematicIP devices on a CCU / OpenCCU, using XML-RPC only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"author": {
|