From 18ea07cc74fff4258d880b9226d0dded8682d113 Mon Sep 17 00:00:00 2001 From: Cansi Karaca Date: Sat, 17 Jan 2026 22:21:48 +0100 Subject: [PATCH 1/2] Uncomment doorbell event emission in openwebnet handler --- lib/handlers/openwebnet-handler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/handlers/openwebnet-handler.js b/lib/handlers/openwebnet-handler.js index bea2d6f..88b0187 100644 --- a/lib/handlers/openwebnet-handler.js +++ b/lib/handlers/openwebnet-handler.js @@ -49,7 +49,7 @@ class OpenwebnetHandler { }, 100); break case msg.startsWith('*8*1#1#4#') ? msg : undefined: - //this.#eventbus.emit('doorbell:pressed', msg) + this.#eventbus.emit('doorbell:pressed', msg) this.#mqtt.dispatchMessage(msg) this.#mqtt.dispatchDoorbellEvent(msg) this.#registry.dispatchEvent('pressed') From f50cd3d1dd8ed2d21439fb1592027bb4dfcaa595 Mon Sep 17 00:00:00 2001 From: Cansi Karaca Date: Sat, 17 Jan 2026 22:22:52 +0100 Subject: [PATCH 2/2] Add automatic lock detection from mymodules configuration - Add DeviceDetector class to read and parse BTicino mymodules file - Auto-detect locks with device IDs, names, visibility, and button IDs - Generate unique lock names using button IDs to avoid HomeKit conflicts - Add exposeInvisibleLocks flag to control visibility of hidden locks - Remove manual doorUnlock and additionalLocks configuration - Update door-unlock API to work with auto-detected locks --- config.js | 34 ++++++++++---- config.json.example | 8 +--- controller-homekit.js | 32 +++++++------ lib/apis/door-unlock.js | 26 +++++------ lib/device-detector.js | 101 ++++++++++++++++++++++++++++++++++++++++ package-lock.json | 12 ++++- 6 files changed, 166 insertions(+), 47 deletions(-) create mode 100644 lib/device-detector.js diff --git a/config.js b/config.js index 1a1aa1f..2cbcf82 100644 --- a/config.js +++ b/config.js @@ -6,6 +6,7 @@ const fs = require("fs") const path = require('path') const utils = require('./lib/utils') +const DeviceDetector = require('./lib/device-detector') const version = require('./package.json').version; const model = utils.model() @@ -14,13 +15,25 @@ const global = { // Use the higher resolution video stream 'highResVideo': model !== 'c100x' } -const doorUnlock = { - // Default behaviour is device ID 20, if you need more, add them to additionalLocks in config.json - openSequence: '*8*19*20##' , - closeSequence: '*8*20*20##', -}; -const additionalLocks = {} +// Expose locks marked as invisible (visible: 0) in mymodules to HomeKit +let exposeInvisibleLocks = false + +// Auto-detect locks from mymodules file +const detector = DeviceDetector.create() +const detectedLocks = detector.detectLocks() + +const locks = {} +for (const lock of detectedLocks) { + const lockKey = `lock-${lock.deviceId}` + locks[lockKey] = { + openSequence: `*8*19*${lock.deviceId}##`, + closeSequence: `*8*20*${lock.deviceId}##`, + name: `${lock.name} ${lock.buttonId}`, + visible: lock.visible, + deviceId: lock.deviceId + } +} const mqtt_config = { // Set to enable to publish events to an external MQTT server @@ -87,8 +100,9 @@ if( detectedPath ) { console.log(`FOUND config.json file at '${detectedPath}' and overriding the values from it.\r\n`) const config = JSON.parse( fs.readFileSync(detectedPath) ) overrideAndPrintValue( "global", global, config.global) - overrideAndPrintValue( "doorUnlock", doorUnlock, config.doorUnlock) - overrideAndPrintValue( "additionalLocks", additionalLocks, config.additionalLocks) + if (config.exposeInvisibleLocks !== undefined) { + exposeInvisibleLocks = config.exposeInvisibleLocks + } overrideAndPrintValue( "mqtt_config", mqtt_config, config.mqtt_config) overrideAndPrintValue( "sip", sip, config.sip) overrideAndPrintValue( "homeassistant", homeassistant, config.homeassistant) @@ -104,9 +118,9 @@ if( global.highResVideo && utils.model() === 'c100x' ) { } console.log(`============================== final config ===================================== -\x1b[33m${JSON.stringify( { global, doorUnlock, additionalLocks, mqtt_config, sip }, null, 2 )}\x1b[0m +\x1b[33m${JSON.stringify( { global, exposeInvisibleLocks, locks, mqtt_config, sip }, null, 2 )}\x1b[0m =================================================================================`) module.exports = { - doorUnlock, additionalLocks, mqtt_config, global, sip, homeassistant, version + locks, mqtt_config, global, exposeInvisibleLocks, sip, homeassistant, version } diff --git a/config.json.example b/config.json.example index 7cb7d93..06366de 100755 --- a/config.json.example +++ b/config.json.example @@ -1,12 +1,6 @@ { "ignoredUnknownValue": {}, - "doorUnlock": { - "openSequence" : "*8*XX*20##" - }, - "additionalLocks": { - "back-door": { "openSequence": "*8*19*21##", "closeSequence": "*8*20*21##" }, - "side-door": { "openSequence": "*8*19*22##", "closeSequence": "*8*20*22##" } - }, + "exposeInvisibleLocks": false, "mqtt_config": { "enabled" : true, "host": "192.168.0.2" diff --git a/controller-homekit.js b/controller-homekit.js index 064f58b..a8e77f1 100644 --- a/controller-homekit.js +++ b/controller-homekit.js @@ -60,29 +60,33 @@ base.eventbus.on('doorbell:pressed', () => { base.eventbus.emit('homekit:pressed') }) -const locks = ["default", ...Object.keys(config.additionalLocks)] +for (const lockKey in config.locks) { + const lock = config.locks[lockKey] + + if (lock.visible === 0 && !config.exposeInvisibleLocks) { + continue + } -for (const lock of locks) { - const doorHomekitSettings = filestore.read(lock, () => { return { 'displayName': lock, 'hidden': false } }) + const doorHomekitSettings = filestore.read(lockKey, () => { + return { 'displayName': lock.name || lockKey, 'hidden': false } + }) - if( doorHomekitSettings && doorHomekitSettings.hidden ) + if (doorHomekitSettings && doorHomekitSettings.hidden) { continue - - let door = config.additionalLocks[lock]; - const { openSequence, closeSequence } = lock === "default" ? { openSequence: config.doorUnlock.openSequence, closeSequence: config.doorUnlock.closeSequence } : { openSequence: door.openSequence, closeSequence: door.closeSequence } + } + + const { openSequence, closeSequence } = lock base.eventbus .on('lock:unlocked:' + openSequence, () => { - //console.log('received lock:unlocked:' + openSequence) - base.eventbus.emit('homekit:locked:' + lock, false) + base.eventbus.emit('homekit:locked:' + lockKey, false) }).on('lock:locked:' + closeSequence, () => { - //console.log('received lock:locked:' + closeSequence) - base.eventbus.emit('homekit:locked:' + lock, true) + base.eventbus.emit('homekit:locked:' + lockKey, true) }) - homekitManager.addLock( lock, doorHomekitSettings.displayName ) - .unlocked( () => { + homekitManager.addLock(lockKey, doorHomekitSettings.displayName) + .unlocked(() => { openwebnet.run("doorUnlock", openSequence, closeSequence) - } ) + }) } homekitManager.addSwitch('Muted' ) diff --git a/lib/apis/door-unlock.js b/lib/apis/door-unlock.js index 004c690..1e2c54d 100644 --- a/lib/apis/door-unlock.js +++ b/lib/apis/door-unlock.js @@ -12,25 +12,21 @@ module.exports = class Api { handle(request, response, url, q) { response.write("
")
-        response.write("Default
") - if( config.additionalLocks ) - { - for( const lock in config.additionalLocks ) - { - response.write("" + lock + "
") + if (config.locks) { + for (const lockKey in config.locks) { + const lock = config.locks[lockKey] + const displayName = lock.name || lockKey + response.write("" + displayName + "
") } } response.write("
") - if( q.id ) { - let door = config.additionalLocks[q.id]; - if( door ) { - openwebnet.run("doorUnlock", door.openSequence, door.closeSequence ) - response.write("Opened lock: " + q.id + "
") - } else if( q.id === "default" ) { - openwebnet.run("doorUnlock", config.doorUnlock.openSequence, config.doorUnlock.closeSequence) - response.write("Opened default lock
") + if (q.id) { + const lock = config.locks[q.id] + if (lock) { + openwebnet.run("doorUnlock", lock.openSequence, lock.closeSequence) + response.write("Opened lock: " + (lock.name || q.id) + "
") } else { - console.error("Door with id: " + q.id + " not found.") + console.error("Lock with id: " + q.id + " not found.") } } } diff --git a/lib/device-detector.js b/lib/device-detector.js new file mode 100644 index 0000000..854fd8f --- /dev/null +++ b/lib/device-detector.js @@ -0,0 +1,101 @@ +const fs = require("fs") +const filestore = require('../json-store') + +const C100X_MODULES = "/home/bticino/cfg/extra/.bt_eliot/mymodules" + +class DeviceDetector { + constructor(mymodulesPath = C100X_MODULES) { + this.mymodulesPath = mymodulesPath + this.devices = null + } + + static create(mymodulesPath) { + return new DeviceDetector(mymodulesPath) + } + + _loadDevices() { + if (this.devices !== null) { + return this.devices + } + + if (!fs.existsSync(this.mymodulesPath)) { + console.log(`[DeviceDetector] mymodules file not found at ${this.mymodulesPath}`) + this.devices = [] + return this.devices + } + + try { + const store = filestore.create(this.mymodulesPath) + this.devices = store.data.modules || [] + console.log(`[DeviceDetector] Loaded ${this.devices.length} devices from ${this.mymodulesPath}`) + } catch (e) { + console.error(`[DeviceDetector] Error reading mymodules file: ${e.message}`) + this.devices = [] + } + + return this.devices + } + + detectCameras() { + const devices = this._loadDevices() + const cameras = devices.filter(m => + m.system === 'videodoorentry' && + m.deviceType === 'EU' && + m.privateAddress?.addressValues?.length > 0 + ).map(m => { + const addressValue = m.privateAddress.addressValues.find(a => a.name === 'address') + return { + id: m.id, + deviceId: addressValue?.value, + name: m.name, + buttonId: m.privateAddress.buttonId, + visible: m.privateAddress.visible + } + }).filter(c => c.deviceId !== undefined) + + console.log(`[DeviceDetector] Detected ${cameras.length} camera(s):`, cameras.map(c => `${c.name} (ID: ${c.deviceId})`).join(', ')) + return cameras + } + + detectLocks() { + const devices = this._loadDevices() + const locks = devices.filter(m => + m.system === 'automation' && + m.device === 'lock' && + m.privateAddress?.addressValues?.length > 0 + ).map(m => { + const addressValue = m.privateAddress.addressValues.find(a => a.name === 'address') + return { + id: m.id, + deviceId: addressValue?.value, + name: m.name, + buttonId: m.privateAddress.buttonId, + visible: m.privateAddress.visible + } + }).filter(l => l.deviceId !== undefined) + + console.log(`[DeviceDetector] Detected ${locks.length} lock(s):`, locks.map(l => `${l.name} (ID: ${l.deviceId})`).join(', ')) + return locks + } + + detectInternalUnit() { + const devices = this._loadDevices() + const internalUnits = devices.filter(m => + m.system === 'videodoorentry' && + m.deviceType === 'IU' + ).map(m => { + return { + id: m.id, + deviceId: m.privateAddress?.addressValues?.find(a => a.name === 'address')?.value, + EUaddress: m.EUaddress + } + }) + + if (internalUnits.length > 0) { + console.log(`[DeviceDetector] Detected ${internalUnits.length} internal unit(s)`) + } + return internalUnits + } +} + +module.exports = DeviceDetector diff --git a/package-lock.json b/package-lock.json index f28ed28..b878e50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -436,6 +436,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -457,6 +458,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -624,6 +626,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -3325,6 +3328,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", "dev": true, + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -3372,6 +3376,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1", @@ -3947,7 +3952,8 @@ "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", - "dev": true + "dev": true, + "peer": true }, "acorn-import-attributes": { "version": "1.9.5", @@ -3961,6 +3967,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "peer": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -4075,6 +4082,7 @@ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.1.tgz", "integrity": "sha512-TUfofFo/KsK/bWZ9TWQ5O26tsWW4Uhmt8IYklbnUa70udB6P2wA7w7o4PY4muaEPBQaAX+CEnmmIA41NVHtPVw==", "dev": true, + "peer": true, "requires": { "caniuse-lite": "^1.0.30001629", "electron-to-chromium": "^1.4.796", @@ -6097,6 +6105,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", "dev": true, + "peer": true, "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -6129,6 +6138,7 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, + "peer": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^2.1.1",