UNPKG

homebridge-shelly-trv

Version:

Homebridge Plugin für Shelly TRV Gen1 Heizkörperthermostate

386 lines (335 loc) 16.2 kB
const ShellyAPIClient = require('./api-client'); class ShellyTRVAccessory { constructor(log, accessory, config, api, platform) { this.log = log; this.accessory = accessory; this.config = config; this.api = api; this.hap = api.hap; this.platform = platform; // Platform-Instanz für Request-Queue // API Client initialisieren this.apiClient = new ShellyAPIClient(log, config.ip, platform); // Logging-Tracking für reduziertes Logging this.lastStatusLog = null; // Service erstellen oder abrufen this.thermostatService = this.accessory.getService(this.hap.Service.Thermostat) || this.accessory.addService(this.hap.Service.Thermostat); // Accessory-Informationen setzen this.accessory.getService(this.hap.Service.AccessoryInformation) .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Shelly') .setCharacteristic(this.hap.Characteristic.Model, 'TRV Gen1') .setCharacteristic(this.hap.Characteristic.SerialNumber, config.ip || 'Unknown'); // Characteristics konfigurieren this.setupCharacteristics(); // Initialisiere Characteristics mit Startwerten (wichtig für HomeKit) this.initializeCharacteristics(); // Status-Polling starten this.startPolling(); } setupCharacteristics() { // Target Temperature (Solltemperatur) this.thermostatService .getCharacteristic(this.hap.Characteristic.TargetTemperature) .setProps({ minValue: 5, maxValue: 35, minStep: 0.5 }) .on('get', this.getTargetTemperature.bind(this)) .on('set', this.setTargetTemperature.bind(this)); // Current Temperature (Aktuelle Temperatur) this.thermostatService .getCharacteristic(this.hap.Characteristic.CurrentTemperature) .setProps({ minValue: -50, maxValue: 100, minStep: 0.1 }) .on('get', this.getCurrentTemperature.bind(this)); // Target Heating Cooling State this.thermostatService .getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState) .setProps({ validValues: [ this.hap.Characteristic.TargetHeatingCoolingState.OFF, this.hap.Characteristic.TargetHeatingCoolingState.HEAT ] }) .on('get', this.getTargetHeatingCoolingState.bind(this)) .on('set', this.setTargetHeatingCoolingState.bind(this)); // Current Heating Cooling State this.thermostatService .getCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState) .on('get', this.getCurrentHeatingCoolingState.bind(this)); // Temperature Display Units (immer Celsius) this.thermostatService .getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits) .on('get', (callback) => { callback(null, this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS); }) .on('set', (value, callback) => { // Immer Celsius verwenden, andere Einheiten ignorieren callback(null); }); // Position State (für Ventil-Position) // Da HomeKit keinen direkten Position-State für Thermostate hat, // nutzen wir CurrentHeatingCoolingState als Indikator } async getTargetTemperature(callback) { try { const status = await this.apiClient.getStatus(); let targetTemp = null; // Prüfe die tatsächliche Struktur: thermostats[0].target_t.value if (status && status.thermostats && status.thermostats[0] && status.thermostats[0].target_t && status.thermostats[0].target_t.value !== undefined) { targetTemp = status.thermostats[0].target_t.value; } else if (status && status.target_t !== undefined) { // Fallback für andere mögliche Strukturen targetTemp = typeof status.target_t === 'object' ? status.target_t.value : status.target_t; } else if (status && status.target_pos !== undefined) { // Fallback: Wenn noch alte API-Struktur vorhanden ist targetTemp = this.targetPosToTemperature(status.target_pos); } if (targetTemp !== null) { this.log(`Target Temperature abgerufen: ${targetTemp}°C`); callback(null, targetTemp); } else { callback(new Error('Keine Target Temperature verfügbar')); } } catch (error) { this.log.error('Fehler beim Abrufen der Target Temperature:', error.message); callback(error); } } async setTargetTemperature(value, callback) { try { this.log(`Setze Target Temperature auf ${value}°C`); await this.apiClient.setTargetTemperature(value); // Aktualisiere sofort den Status setTimeout(() => this.updateStatus(), 1000); callback(null); } catch (error) { this.log.error('Fehler beim Setzen der Target Temperature:', error.message); callback(error); } } async getCurrentTemperature(callback) { try { const status = await this.apiClient.getStatus(); let temperature = null; // Prüfe die tatsächliche Struktur: thermostats[0].tmp.value if (status && status.thermostats && status.thermostats[0] && status.thermostats[0].tmp && status.thermostats[0].tmp.value !== undefined) { temperature = status.thermostats[0].tmp.value; } else if (status && status.temp !== undefined) { // Fallback für andere mögliche Strukturen temperature = typeof status.temp === 'object' ? status.temp.value : status.temp; } if (temperature !== null) { this.log(`Current Temperature abgerufen: ${temperature}°C`); callback(null, temperature); } else { callback(new Error('Keine Current Temperature verfügbar')); } } catch (error) { this.log.error('Fehler beim Abrufen der Current Temperature:', error.message); callback(error); } } async getTargetHeatingCoolingState(callback) { try { const status = await this.apiClient.getStatus(); if (status) { // Prüfe ob Thermostat aktiviert ist: thermostats[0].target_t.enabled let isEnabled = false; if (status.thermostats && status.thermostats[0] && status.thermostats[0].target_t && status.thermostats[0].target_t.enabled !== undefined) { isEnabled = status.thermostats[0].target_t.enabled === true; } else if (status.target_t_enabled !== undefined) { isEnabled = status.target_t_enabled === 1 || status.target_t_enabled === true; } else if (status.target_t !== undefined) { // Wenn target_t gesetzt ist und > 5°C, dann ist es aktiviert const targetTemp = typeof status.target_t === 'object' ? status.target_t.value : status.target_t; isEnabled = targetTemp > 5; } else if (status.target_pos !== undefined && status.target_pos > 0) { // Fallback für alte API-Struktur isEnabled = true; } const state = isEnabled ? this.hap.Characteristic.TargetHeatingCoolingState.HEAT : this.hap.Characteristic.TargetHeatingCoolingState.OFF; callback(null, state); } else { callback(null, this.hap.Characteristic.TargetHeatingCoolingState.OFF); } } catch (error) { this.log.error('Fehler beim Abrufen des Target Heating Cooling State:', error.message); callback(null, this.hap.Characteristic.TargetHeatingCoolingState.OFF); } } async setTargetHeatingCoolingState(value, callback) { try { if (value === this.hap.Characteristic.TargetHeatingCoolingState.OFF) { // Thermostat deaktivieren (target_t_enabled=0) // Setze niedrige Temperatur als deaktiviert this.log('Deaktiviere Thermostat (Heizung aus)'); await this.apiClient.setTargetTemperature(5); } else if (value === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { // Thermostat aktivieren (auf vorherige Temperatur oder Standard) this.log('Aktiviere Thermostat (Heizung an)'); const currentStatus = await this.apiClient.getStatus(); let targetTemp = 20; // Standard // Versuche aktuelle target_t zu lesen: thermostats[0].target_t.value if (currentStatus && currentStatus.thermostats && currentStatus.thermostats[0] && currentStatus.thermostats[0].target_t && currentStatus.thermostats[0].target_t.value !== undefined) { const currentTarget = currentStatus.thermostats[0].target_t.value; if (currentTarget > 5) { targetTemp = currentTarget; } } else if (currentStatus && currentStatus.target_t !== undefined) { const currentTarget = typeof currentStatus.target_t === 'object' ? currentStatus.target_t.value : currentStatus.target_t; if (currentTarget > 5) { targetTemp = currentTarget; } } await this.apiClient.setTargetTemperature(targetTemp); } setTimeout(() => this.updateStatus(), 1000); callback(null); } catch (error) { this.log.error('Fehler beim Setzen des Target Heating Cooling State:', error.message); callback(error); } } async getCurrentHeatingCoolingState(callback) { try { const status = await this.apiClient.getStatus(); if (status) { // Zeige Heizstatus basierend auf pos (Ventilposition): thermostats[0].pos let valvePos = 0; if (status.thermostats && status.thermostats[0] && status.thermostats[0].pos !== undefined) { valvePos = status.thermostats[0].pos; } else if (status.valve_pos !== undefined) { valvePos = status.valve_pos; } const state = valvePos > 10 // Wenn Ventil mehr als 10% geöffnet ist ? this.hap.Characteristic.CurrentHeatingCoolingState.HEAT : this.hap.Characteristic.CurrentHeatingCoolingState.OFF; callback(null, state); } else { callback(null, this.hap.Characteristic.CurrentHeatingCoolingState.OFF); } } catch (error) { this.log.error('Fehler beim Abrufen des Current Heating Cooling State:', error.message); callback(null, this.hap.Characteristic.CurrentHeatingCoolingState.OFF); } } // Hilfsfunktionen für Temperatur-Konvertierung (Fallback für alte API-Struktur) targetPosToTemperature(targetPos) { // target_pos 0-100 → Temperatur 5-35°C // Formel: temp = 5 + (target_pos * 0.3) // Diese Funktion wird nur als Fallback verwendet, wenn die neue API-Struktur nicht verfügbar ist return Math.round((5 + (targetPos * 0.3)) * 2) / 2; // Runden auf 0.5 } // Initialisiere Characteristics mit Startwerten async initializeCharacteristics() { try { // Setze initiale Werte, damit HomeKit die Temperatursteuerung erkennt this.thermostatService .updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS); // Versuche aktuelle Werte zu laden (beim Start frische Daten holen) const status = await this.apiClient.getStatus(true); if (status && status.thermostats && status.thermostats[0]) { const thermostat = status.thermostats[0]; if (thermostat.target_t && thermostat.target_t.value !== undefined) { this.thermostatService .updateCharacteristic(this.hap.Characteristic.TargetTemperature, thermostat.target_t.value); } if (thermostat.tmp && thermostat.tmp.value !== undefined) { this.thermostatService .updateCharacteristic(this.hap.Characteristic.CurrentTemperature, thermostat.tmp.value); } const isEnabled = thermostat.target_t && thermostat.target_t.enabled === true; const targetState = isEnabled ? this.hap.Characteristic.TargetHeatingCoolingState.HEAT : this.hap.Characteristic.TargetHeatingCoolingState.OFF; this.thermostatService .updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, targetState); const valvePos = thermostat.pos !== undefined ? thermostat.pos : 0; const currentState = valvePos > 10 ? this.hap.Characteristic.CurrentHeatingCoolingState.HEAT : this.hap.Characteristic.CurrentHeatingCoolingState.OFF; this.thermostatService .updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, currentState); } } catch (error) { this.log.error('Fehler beim Initialisieren der Characteristics:', error.message); // Setze zumindest Default-Werte this.thermostatService .updateCharacteristic(this.hap.Characteristic.TargetTemperature, 20) .updateCharacteristic(this.hap.Characteristic.CurrentTemperature, 20); } } // Status-Polling startPolling() { // Initiale Status-Aktualisierung this.updateStatus(); // Polling alle 60 Sekunden (reduziert Traffic erheblich) // Die Getter-Methoden nutzen den Cache, daher ist häufiges Polling nicht nötig this.pollInterval = setInterval(() => { this.updateStatus(); }, 60000); // 60 Sekunden statt 15 } async updateStatus() { try { // Beim Polling nutzen wir den Cache, wenn er noch gültig ist (weniger Traffic) // Nur wenn Cache abgelaufen ist, holen wir neue Daten const status = await this.apiClient.getStatus(false); if (status && status.thermostats && status.thermostats[0]) { const thermostat = status.thermostats[0]; // Aktualisiere Current Temperature: thermostats[0].tmp.value if (thermostat.tmp && thermostat.tmp.value !== undefined) { const currentTemp = thermostat.tmp.value; this.thermostatService .updateCharacteristic(this.hap.Characteristic.CurrentTemperature, currentTemp); } // Aktualisiere Target Temperature: thermostats[0].target_t.value if (thermostat.target_t && thermostat.target_t.value !== undefined) { const targetTemp = thermostat.target_t.value; this.thermostatService .updateCharacteristic(this.hap.Characteristic.TargetTemperature, targetTemp); } // Update Heating Cooling States: thermostats[0].target_t.enabled let isEnabled = false; if (thermostat.target_t && thermostat.target_t.enabled !== undefined) { isEnabled = thermostat.target_t.enabled === true; } const targetState = isEnabled ? this.hap.Characteristic.TargetHeatingCoolingState.HEAT : this.hap.Characteristic.TargetHeatingCoolingState.OFF; this.thermostatService .updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, targetState); // Current State basierend auf Ventilposition: thermostats[0].pos const valvePos = thermostat.pos !== undefined ? thermostat.pos : 0; const currentState = valvePos > 10 ? this.hap.Characteristic.CurrentHeatingCoolingState.HEAT : this.hap.Characteristic.CurrentHeatingCoolingState.OFF; this.thermostatService .updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, currentState); // Debug-Logging reduziert - nur bei Änderungen oder alle 5 Minuten const now = Date.now(); if (!this.lastStatusLog || (now - this.lastStatusLog) > 300000) { // Alle 5 Minuten const currentTemp = thermostat.tmp && thermostat.tmp.value !== undefined ? thermostat.tmp.value : 'N/A'; const targetTemp = thermostat.target_t && thermostat.target_t.value !== undefined ? thermostat.target_t.value : 'N/A'; this.log.debug(`Status aktualisiert - Temp: ${currentTemp}°C, Target: ${targetTemp}°C, Valve: ${valvePos}%, Enabled: ${isEnabled}`); this.lastStatusLog = now; } } } catch (error) { this.log.error('Fehler beim Status-Update:', error.message); } } // Cleanup beim Entfernen des Accessories shutdown() { if (this.pollInterval) { clearInterval(this.pollInterval); } } } module.exports = ShellyTRVAccessory;