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 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
  [![Release](https://github.com/california444/homebridge-hap-homematic/actions/workflows/release.yml/badge.svg)](https://github.com/california444/homebridge-hap-homematic/actions/workflows/release.yml)
9
6
  [![License: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
@@ -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, discovered ? 'discovered on the CCU' : 'not discovered yet')}
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
- async function onDiscover() {
1013
- spinner(true);
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 with
1102
- // a background Discover so the list fills in on its own.
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
- // Fire-and-forget; failures fall back to the empty state with a
1109
- // hint already in place. No spinner — user can still navigate.
1110
- onDiscover().catch(() => { /* errors already toasted by onDiscover */ });
1142
+ loadCachedDiscovery()
1143
+ .then((ok) => (ok ? undefined : onDiscover({ quiet: true })))
1144
+ .catch(() => { /* errors already toasted by onDiscover */ });
1111
1145
  }
1112
1146
  }
1113
1147
 
@@ -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
- const enrichedDevices = devices.map((d) => ({
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.1",
4
- "description": "Homebridge plugin for HomeMatic / HomematicIP devices on a CCU / OpenCCU, using XML-RPC only. Hard fork of matthiaseinig/Homebridge-Homematic-HAP (homebridge-homematic-hap); not affiliated with thkl/hap-homematic.",
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": {