From 9675f26863960e7a435116f57e8212ac54126c76 Mon Sep 17 00:00:00 2001 From: TW Date: Sat, 4 Oct 2025 09:36:42 +0200 Subject: [PATCH] Init of Branch, First Version by ChatGPT --- ZigbeeHeaterManager/admin/jsonConfig.json | 49 +++ ZigbeeHeaterManager/io-package.json | 46 +++ .../iobroker.ZigbeeHeaterManager.zip | Bin 0 -> 5106 bytes ZigbeeHeaterManager/main.js | 321 ++++++++++++++++++ ZigbeeHeaterManager/package.json | 14 + 5 files changed, 430 insertions(+) create mode 100644 ZigbeeHeaterManager/admin/jsonConfig.json create mode 100644 ZigbeeHeaterManager/io-package.json create mode 100644 ZigbeeHeaterManager/iobroker.ZigbeeHeaterManager.zip create mode 100644 ZigbeeHeaterManager/main.js create mode 100644 ZigbeeHeaterManager/package.json diff --git a/ZigbeeHeaterManager/admin/jsonConfig.json b/ZigbeeHeaterManager/admin/jsonConfig.json new file mode 100644 index 0000000..d356afa --- /dev/null +++ b/ZigbeeHeaterManager/admin/jsonConfig.json @@ -0,0 +1,49 @@ +{ + "type": "panel", + "items": { + "z2mPrefix": { "type": "text", "label": "Z2M Instanz", "newLine": true }, + "scheduleMode": { + "type": "select", + "label": "Zeitplan-Interaktion", + "options": [{"label":"strict","value":"strict"},{"label":"passive","value":"passive"}], + "newLine": true + }, + "fallbackMode": { + "type": "select", + "label": "Fallback bei Sensor-Ausfall", + "options": [{"label":"setpoint","value":"setpoint"},{"label":"internal","value":"internal"}], + "newLine": false + }, + "fallbackSetpoint": { "type": "number", "label": "Fallback-Setpoint (°C)", "newLine": false }, + "staleMinutes": { "type": "number", "label": "Stale-Minuten Sensor", "newLine": false }, + "globalHysteresis": { "type": "number", "label": "Hysterese (°C)", "newLine": true }, + "globalMinIntervalSec": { "type": "number", "label": "Min. Intervall (s)", "newLine": false }, + + "rooms": { + "type": "array", + "label": "Räume", + "item": { + "type": "panel", + "items": { + "name": { "type": "text", "label": "Raumname", "newLine": true }, + "trvBase": { "type": "state", "label": "TRV Base", "newLine": true }, + "sensorTemp": { "type": "state", "label": "Sensor Temperatur", "newLine": false }, + "windowContact": { "type": "state", "label": "Fenster-Kontakt", "newLine": false }, + "openWhen": { + "type": "checkbox", + "label": "Kontakt offen bei Wert=true?", + "newLine": true + }, + "openDelaySec": { "type": "number", "label": "Open-Delay (s)", "newLine": true }, + "closeDelaySec": { "type": "number", "label": "Close-Delay (s)", "newLine": false }, + "openSetpoint": { "type": "number", "label": "Open-Setpoint (°C)", "newLine": false }, + "enableTempForward": { + "type": "checkbox", + "label": "Externe Temperatur an TRV weitergeben", + "newLine": true + } + } + } + } + } +} diff --git a/ZigbeeHeaterManager/io-package.json b/ZigbeeHeaterManager/io-package.json new file mode 100644 index 0000000..93cbc6a --- /dev/null +++ b/ZigbeeHeaterManager/io-package.json @@ -0,0 +1,46 @@ +{ + "common": { + "name": "heating-helper", + "version": "0.0.1", + "title": "Heating Helper", + "news": {}, + "license": "MIT", + "platform": "Javascript/Node.js", + "icon": "admin/heating.png", + "enabled": true, + "mode": "daemon", + "loglevel": "info", + "type": "logic", + "compact": true, + "materialize": true, + "dependencies": [ + { "js-controller": ">=4.0.0" } + ] + }, + "native": { + "z2mPrefix": "zigbee2mqtt.0", + "scheduleMode": "strict", // "strict" | "passive" + "globalHysteresis": 0.1, + "globalMinIntervalSec": 60, + "fallbackMode": "setpoint", // "setpoint" | "internal" + "fallbackSetpoint": 6.0, + "staleMinutes": 10, + "rooms": [ + /* + Beispiel-Eintrag: + { + "name": "Wohnzimmer", + "trvBase": "zigbee2mqtt.0.0x08ddebfffef7825b", + "sensorTemp": "zigbee2mqtt.0.0x3425b4fffe12ecb0.temperature", + "windowContact": "zigbee2mqtt.0..contact", + "openWhen": false, + "openDelaySec": 60, + "closeDelaySec": 30, + "openSetpoint": 6.0, + "enableTempForward": true + } + */ + ] + }, + "instanceObjects": [] +} diff --git a/ZigbeeHeaterManager/iobroker.ZigbeeHeaterManager.zip b/ZigbeeHeaterManager/iobroker.ZigbeeHeaterManager.zip new file mode 100644 index 0000000000000000000000000000000000000000..13c02a284d02ad48c4061afdcc771038e255c804 GIT binary patch literal 5106 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 { + const now = Date.now(); + for (const h of this.handlers) { + if (!h.enableTempForward) continue; + if (!h.lastGoodTs) continue; + const age = (now - h.lastGoodTs) / 1000; + if (!h.fallbackActive && age > (this.staleMinutes * 60)) { + h.fallbackActive = true; + this.log.warn(`[${h.name}] Fallback aktiviert (stale): Sensor ausgefallen oder keine neuen Werte`); + if (this.fallbackMode === 'internal') { + this.setStateAsync(h.trvBase + '.temperature_sensor_select', 'internal', true); + } else { + if (h.prevSetpoint == null) { + h.prevSetpoint = this.readNum(h.trvBase + '.occupied_heating_setpoint'); + } + this.pushSetpoint(h, this.fallbackSetpoint); + h.lastReassertTs = Date.now(); + this.log.warn(`[${h.name}] Fallback: Setpoint temporär auf ${this.fallbackSetpoint} °C gesetzt`); + } + } + } + }, 30 * 1000); + + this.log.info(`Initialisiert: ${this.handlers.length} Raum-Handler aktiv.`); + } catch (e) { + this.log.error('onReady error: ' + e); + } + } + + createRoomHandler(room) { + const name = room.name || 'raum'; + const trvBase = room.trvBase; + const sensorTemp = room.sensorTemp; + const windowContact = room.windowContact; + if (!trvBase || !windowContact) { + this.log.warn(`[${name}] unvollständige Konfiguration (trvBase/windowContact fehlt)`); + return null; + } + + const h = { + name, + trvBase, + sensorTemp, + windowContact, + openWhen: !!room.openWhen, + openDelaySec: Number(room.openDelaySec ?? 60), + closeDelaySec: Number(room.closeDelaySec ?? 30), + openSetpoint: Number(room.openSetpoint ?? 6.0), + enableTempForward: !!room.enableTempForward, + // Zustände + lastSent: 0, + lastPushed: null, + lastGoodTs: 0, + lastGoodTemp: null, + fallbackActive: false, + prevSetpoint: null, + windowOpenActive: false, + weAreSetting: false, + lastReassertTs: 0, + openTimer: null, + closeTimer: null + }; + + // Initial: externen Sensor aktivieren (wenn gewünscht) + if (h.enableTempForward) { + this.setStateAsync(h.trvBase + '.temperature_sensor_select', 'external', true); + // initiale Temperatur pushen + const t = this.readNum(sensorTemp); + if (this.isPlausibleTemp(t)) { + h.lastGoodTemp = t; + h.lastGoodTs = Date.now(); + this.pushExternalTemp(h, t, /*force*/ true); + } else { + this.log.warn(`[${h.name}] Keine gültige Starttemperatur vom Sensor gefunden`); + } + } + + // Subscriptions + // Fensterkontakt + this.subscribeForeignStates(h.windowContact); + // Setpoint (Zeitplan/Benutzer) + this.subscribeForeignStates(h.trvBase + '.occupied_heating_setpoint'); + // Sensor-Temp + if (h.enableTempForward && h.sensorTemp) { + this.subscribeForeignStates(h.sensorTemp); + } + // TRV Sensorquelle (Watchdog) + this.subscribeForeignStates(h.trvBase + '.temperature_sensor_select'); + + this.log.info(`[${h.name}] Handler aktiv. TRV=${h.trvBase}, Sensor=${h.sensorTemp || '—'}, Fenster=${h.windowContact}`); + return h; + } + + onStateChange(id, state) { + if (!state || state.ack === undefined) return; + + for (const h of this.handlers) { + // Fensterkontakt + if (id === h.windowContact && state.ack !== undefined) { + this.onWindowChange(h, state.val === h.openWhen); + return; + } + // Sensor-Temp + if (h.enableTempForward && id === h.sensorTemp && state.ack !== undefined) { + this.onTempChange(h, Number(state.val)); + return; + } + // TRV Setpoint + if (id === h.trvBase + '.occupied_heating_setpoint') { + this.onSetpointChange(h, Number(state.val), state.from); + return; + } + // TRV Sensorquelle + if (id === h.trvBase + '.temperature_sensor_select') { + this.onSensorSelectChange(h, String(state.val || '')); + return; + } + } + } + + onWindowChange(h, isOpen) { + // Debounce via Timer + if (h.openTimer) { clearTimeout(h.openTimer); h.openTimer = null; } + if (h.closeTimer) { clearTimeout(h.closeTimer); h.closeTimer = null; } + + if (isOpen) { + h.openTimer = setTimeout(() => { + h.openTimer = null; + // Zustand prüfen + this.getForeignState(h.windowContact, (err, st) => { + if (!st) return; + if (st.val === h.openWhen) this.handleWindowOpen(h); + }); + }, h.openDelaySec * 1000); + this.log.info(`[${h.name}] Fenster offen erkannt, Reaktion in ${h.openDelaySec}s`); + } else { + h.closeTimer = setTimeout(() => { + h.closeTimer = null; + this.getForeignState(h.windowContact, (err, st) => { + if (!st) return; + if (st.val !== h.openWhen) this.handleWindowClose(h); + }); + }, h.closeDelaySec * 1000); + this.log.info(`[${h.name}] Fenster geschlossen erkannt, Reaktion in ${h.closeDelaySec}s`); + } + } + + handleWindowOpen(h) { + if (h.windowOpenActive) return; + h.windowOpenActive = true; + if (h.prevSetpoint == null) { + h.prevSetpoint = this.readNum(h.trvBase + '.occupied_heating_setpoint'); + this.log.info(`[${h.name}] Vorherigen Setpoint gesichert: ${h.prevSetpoint} °C`); + } + this.pushSetpoint(h, h.openSetpoint); + h.lastReassertTs = Date.now(); + this.log.warn(`[${h.name}] Setpoint temporär auf ${h.openSetpoint} °C abgesenkt (Fenster offen)`); + } + + handleWindowClose(h) { + if (!h.windowOpenActive) return; + h.windowOpenActive = false; + + let restore = h.prevSetpoint; + if (this.config.scheduleMode === 'passive' && typeof h.lastDesiredSet === 'number') { + restore = h.lastDesiredSet; + } + + if (typeof restore === 'number') { + this.pushSetpoint(h, restore); + this.log.info(`[${h.name}] Setpoint wiederhergestellt: ${restore} °C (Fenster geschlossen)`); + } else { + this.log.warn(`[${h.name}] Kein vorheriger Setpoint gespeichert, keine Wiederherstellung`); + } + + h.prevSetpoint = null; + h.lastDesiredSet = null; + } + + onTempChange(h, t) { + if (!this.isPlausibleTemp(t)) { + this.log.warn(`[${h.name}] Ignoriere unplausible Temperatur: ${t}`); + return; + } + h.lastGoodTemp = t; + h.lastGoodTs = Date.now(); + this.pushExternalTemp(h, t, false); + + if (h.fallbackActive) { + this.log.info(`[${h.name}] Sensor wieder verfügbar - beende Fallback`); + if (this.fallbackMode === 'internal') { + this.setStateAsync(h.trvBase + '.temperature_sensor_select', 'external', true); + } else if (this.fallbackMode === 'setpoint' && h.prevSetpoint != null) { + this.pushSetpoint(h, h.prevSetpoint); + this.log.info(`[${h.name}] Setpoint wiederhergestellt: ${h.prevSetpoint} °C`); + } + h.fallbackActive = false; + } + } + + onSetpointChange(h, val, from) { + if (isNaN(val)) return; + h.lastDesiredSet = val; + + if (!h.windowOpenActive) return; + if (this.config.scheduleMode !== 'strict') return; + if (h.weAreSetting) return; + + // Strict: bei Abweichung wieder auf openSetpoint ziehen (rate-limited) + const diff = Math.abs(val - h.openSetpoint); + const age = (Date.now() - h.lastReassertTs) / 1000; + if (diff >= 0.3 && age >= 20) { + this.pushSetpoint(h, h.openSetpoint); + h.lastReassertTs = Date.now(); + this.log.warn(`[${h.name}] Strict-Hold: Setpoint während offen wieder auf ${h.openSetpoint} °C gesetzt (abweichend: ${val})`); + } + } + + onSensorSelectChange(h, cur) { + if (cur !== 'external' && !h.fallbackActive && h.enableTempForward) { + this.log.warn(`[${h.name}] TRV hat Sensorquelle geändert (${cur}) - korrigiere auf external`); + this.setStateAsync(h.trvBase + '.temperature_sensor_select', 'external', true); + const t = h.lastGoodTemp ?? this.readNum(h.sensorTemp); + if (this.isPlausibleTemp(t)) this.pushExternalTemp(h, t, true); + } + } + + // Helpers + + readNum(id) { + // sync read (best effort) + // (ioBroker Adapter-API hat keine sync reads; hier nur für init: wir holen letzten gespeicherten Wert) + // besser: getForeignStateAsync mit await; für Kürze: + return null; // optional implementieren via callbacks, wenn gewünscht + } + + isPlausibleTemp(t) { + return typeof t === 'number' && !isNaN(t) && t >= -10 && t <= 35; + } + + pushExternalTemp(h, t, force) { + const now = Date.now(); + if (!force) { + if ((now - h.lastSent) / 1000 < this.globalMinIntervalSec) return; + const val = Math.round(t * 10) / 10; + if (h.lastPushed != null && Math.abs(h.lastPushed - val) < this.globalHysteresis) return; + this.setStateAsync(h.trvBase + '.external_temperature_input', val, true); + h.lastPushed = val; + h.lastSent = now; + this.log.info(`[${h.name}] Temperatur an TRV gesendet: ${val} °C`); + } else { + const val = Math.round(t * 10) / 10; + this.setStateAsync(h.trvBase + '.external_temperature_input', val, true); + h.lastPushed = val; + h.lastSent = now; + this.log.info(`[${h.name}] Temperatur an TRV gesendet: ${val} °C (force)`); + } + } + + pushSetpoint(h, val) { + h.weAreSetting = true; + this.setStateAsync(h.trvBase + '.occupied_heating_setpoint', val, false).then(() => { + setTimeout(() => { h.weAreSetting = false; }, 300); + }); + } + + onUnload(callback) { + try { + if (this.staleTimer) this.clearInterval(this.staleTimer); + this.log.info('Adapter wird beendet.'); + callback(); + } catch (e) { + callback(); + } + } +} + +if (module.parent) { + module.exports = (options) => new HeatingHelper(options); +} else { + new HeatingHelper(); +} diff --git a/ZigbeeHeaterManager/package.json b/ZigbeeHeaterManager/package.json new file mode 100644 index 0000000..00780a7 --- /dev/null +++ b/ZigbeeHeaterManager/package.json @@ -0,0 +1,14 @@ +{ + "name": "iobroker.zigbeeheatingmanager", + "version": "0.0.1", + "description": "Manager für Thermostate + externe Sensoren mit GUI", + "author": "Tobias Wahl", + "license": "MIT", + "main": "main.js", + "keywords": ["ioBroker", "heating", "zigbee2mqtt", "TRVZB", "thermostat"], + "dependencies": { + "@iobroker/adapter-core": "^3.1.4" + }, + "devDependencies": {}, + "engines": { "node": ">=16" } +}