UNPKG

iobroker.energy-schedule

Version:

Energy scheduling adapter using Corrently GrünstromIndex

435 lines (371 loc) 16.2 kB
// main.js "use strict"; const utils = require("@iobroker/adapter-core"); const CorrentlyClient = require('corrently-api'); class EnergySchedule extends utils.Adapter { constructor(options) { super({ ...options, name: "energy-schedule", }); this.scheduleCheckInterval = null; this.currentSchedule = null; this.client = null; this.on("ready", this.onReady.bind(this)); this.on("stateChange", this.onStateChange.bind(this)); this.on("unload", this.onUnload.bind(this)); } async onReady() { try { this.log.info("Starting energy-schedule adapter..."); // Set connection state to false at startup await this.setStateAsync('info.connection', { val: false, ack: true }); this.log.debug(`Config values: ${JSON.stringify(this.config)}`); // Validate ZIP code if (!this.config.zip) { this.log.error("No ZIP code configured!"); return; } // Initialize client configuration const clientConfig = { appid: "0xb61DD55F0DDA7C17975a82dd18EAeD8C025a64ea", zip: this.config.zip // Add ZIP to client config }; if (this.config.personalToken) { clientConfig.token = this.config.personalToken; this.log.debug("Using personal token from config"); } else { this.log.info("No personal token configured, using default token"); } try { // Initialize Corrently client this.log.debug(`Initializing Corrently client with config: ${JSON.stringify(clientConfig)}`); this.client = new CorrentlyClient(clientConfig); // Test the connection with correct method and ZIP const testConnection = await this.client.strommix.getEnergyMix({ zip: this.config.zip, timeframe: "last7days" }); if (!testConnection) { throw new Error("No response from API"); } this.log.debug(`API Test response received: ${JSON.stringify(testConnection)}`); // Mark connection as established await this.setStateAsync('info.connection', { val: true, ack: true }); this.log.info("Successfully connected to Corrently API"); // Create states after successful connection await this.createStates(); // Start schedule check interval this.scheduleCheckInterval = setInterval(() => { this.checkScheduleStatus(); }, 60000); // Check every minute // Initial schedule creation if requirements are set await this.createInitialSchedule(); } catch (apiError) { this.log.error(`Failed to connect to Corrently API: ${apiError.message}`); if (apiError.response) { this.log.error(`API Response: ${JSON.stringify(apiError.response.data)}`); } await this.setStateAsync('info.connection', { val: false, ack: true }); return; } } catch (error) { this.log.error(`Initialization error: ${error.message}`); if (error.stack) this.log.debug(`Stack: ${error.stack}`); await this.setStateAsync('info.connection', { val: false, ack: true }); } } async createStates() { try { // Erstelle zuerst die Channels await this.setObjectNotExistsAsync("requirements", { type: "channel", common: { name: "Schedule Requirements" }, native: {} }); await this.setObjectNotExistsAsync("status", { type: "channel", common: { name: "Schedule Status" }, native: {} }); await this.setObjectNotExistsAsync("control", { type: "channel", common: { name: "Schedule Control" }, native: {} }); // Warte kurz await new Promise(resolve => setTimeout(resolve, 1000)); // Erstelle dann die States const states = { "requirements.energyDemand": { name: "Energy Demand", type: "number", role: "value", unit: "kWh", read: true, write: true }, "requirements.maxLoad": { name: "Maximum Load", type: "number", role: "value", unit: "W", read: true, write: true }, "requirements.avgLoad": { name: "Average Load", type: "number", role: "value", unit: "W", read: true, write: true }, "status.isActive": { name: "Schedule Active", type: "boolean", role: "indicator", read: true, write: false }, "status.nextSwitch": { name: "Next Switch Time", type: "string", role: "text", read: true, write: false }, "status.scheduleId": { name: "Current Schedule ID", type: "string", role: "text", read: true, write: false }, "status.scheduleDetails": { name: "Complete Schedule Details", type: "string", role: "json", read: true, write: false }, "control.createSchedule": { name: "Create New Schedule", type: "boolean", role: "button", read: false, write: true } }; // Erstelle alle States for (const [id, common] of Object.entries(states)) { await this.setObjectNotExistsAsync(id, { type: "state", common: common, native: {} }); } this.log.info("All states created successfully"); } catch (error) { this.log.error(`Error creating states: ${error.message}`); throw error; } } async createInitialSchedule() { try { // Warte kurz, bis alle States erstellt sind await new Promise(resolve => setTimeout(resolve, 2000)); this.log.info("Checking for existing requirements..."); const energyDemand = await this.getStateAsync("requirements.energyDemand"); const maxLoad = await this.getStateAsync("requirements.maxLoad"); const avgLoad = await this.getStateAsync("requirements.avgLoad"); // Prüfe, ob die States existieren UND Werte haben if (energyDemand && maxLoad && avgLoad && energyDemand.val !== null && maxLoad.val !== null && avgLoad.val !== null) { this.log.info(`Found existing requirements: Energy=${energyDemand.val}, Max=${maxLoad.val}, Avg=${avgLoad.val}`); // Erstelle Schedule mit den gefundenen Werten await this.createSchedule({ requirements: { energyDemand: energyDemand.val, maxLoad: maxLoad.val, avgLoad: avgLoad.val } }); } else { this.log.info("No complete requirements found for initial schedule"); // Setze Default-Werte await this.setStateAsync("requirements.energyDemand", { val: 10, ack: true }); await this.setStateAsync("requirements.maxLoad", { val: 3500, ack: true }); await this.setStateAsync("requirements.avgLoad", { val: 2000, ack: true }); // Erstelle Schedule mit Default-Werten await this.createSchedule({ requirements: { energyDemand: 10, maxLoad: 3500, avgLoad: 2000 } }); } } catch (error) { this.log.error(`Error in initial schedule creation: ${error.message}`); if (error.stack) this.log.debug(`Stack: ${error.stack}`); } } async createSchedule(requirements) { try { this.log.info(`Creating schedule with requirements: ${JSON.stringify(requirements)}`); if (!this.config.zip) { this.log.error("No ZIP code configured!"); return; } const mergedRequirements = { zip: this.config.zip, requirements: { law: this.config.law || "comfort", activeHours: this.config.activeHours || 1, consecutiveHours: this.config.consecutiveHours !== false, coverageHours: this.config.coverageHours || 23, ...requirements.requirements } }; this.log.debug(`Merged requirements: ${JSON.stringify(mergedRequirements)}`); // Create new schedule this.log.info('Calling API to create schedule...'); const newSchedule = await this.client.schedule.createSchedule(mergedRequirements); this.log.info(`Schedule created with ID: ${newSchedule.scheduleId}`); // Store scheduleId immediately await this.setStateAsync("status.scheduleId", { val: newSchedule.scheduleId, ack: true }); // Get full schedule details this.log.info('Fetching schedule details...'); this.currentSchedule = await this.client.schedule.getSchedule(newSchedule.scheduleId); this.log.info(`Retrieved schedule details: ${JSON.stringify(this.currentSchedule)}`); // Update all states await this.updateScheduleStates(); } catch (error) { this.log.error(`Schedule creation error: ${error.message}`); if (error.response) { this.log.error(`API Response: ${JSON.stringify(error.response.data)}`); } // Clear scheduleId on error await this.setStateAsync("status.scheduleId", { val: null, ack: true }); } } // Verbesserte checkScheduleStatus Funktion async checkScheduleStatus() { if (!this.currentSchedule) { this.log.debug('No schedule to check'); return; } try { const scheduleId = this.currentSchedule.scheduleId; this.log.debug(`Checking status for schedule ${scheduleId}`); // Refresh schedule details this.currentSchedule = await this.client.schedule.getSchedule(scheduleId); await this.updateScheduleStates(); } catch (error) { this.log.error(`Status check error: ${error.message}`); this.currentSchedule = null; await this.updateScheduleStates(); // This will clear all states } } isCurrentlyActive(schedule) { try { // Prüfe ob schedule und seine Eigenschaften existieren if (!schedule?.schedule?.currentRecommendation?.currentPowerState) { this.log.warn('No currentPowerState found in currentRecommendation'); return false; } return schedule.schedule.currentRecommendation.currentPowerState === 'on'; } catch (error) { this.log.error(`Error in isCurrentlyActive: ${error.message}`); return false; } } findNextSwitchTime(schedule) { try { // Prüfe ob schedule und seine Eigenschaften existieren if (!schedule?.schedule?.currentRecommendation?.nextSwitch) { this.log.warn('No nextSwitch found in currentRecommendation'); return new Date(); } return new Date(schedule.schedule.currentRecommendation.nextSwitch); } catch (error) { this.log.error(`Error in findNextSwitchTime: ${error.message}`); return new Date(); } } // Verbesserte updateScheduleStates Funktion async updateScheduleStates() { try { if (!this.currentSchedule) { this.log.warn('No current schedule available for status update'); await this.setStateAsync("status.isActive", { val: null, ack: true }); await this.setStateAsync("status.nextSwitch", { val: null, ack: true }); await this.setStateAsync("status.scheduleDetails", { val: null, ack: true }); return; } const isActive = this.isCurrentlyActive(this.currentSchedule); const nextSwitch = this.findNextSwitchTime(this.currentSchedule); this.log.debug(`Status update - isActive: ${isActive}, nextSwitch: ${nextSwitch}, currentPowerState: ${this.currentSchedule.schedule.currentRecommendation.currentPowerState}`); const updatePromises = [ this.setStateAsync("status.isActive", { val: isActive, ack: true }), this.setStateAsync("status.nextSwitch", { val: nextSwitch.toISOString(), ack: true }), this.setStateAsync("status.scheduleDetails", { val: JSON.stringify(this.currentSchedule), ack: true }), this.setStateAsync("status.scheduleId", { val: this.currentSchedule.scheduleId, ack: true }) ]; await Promise.all(updatePromises); this.log.debug('States updated successfully'); } catch (error) { this.log.error(`Error updating states: ${error.message}`); } } async onStateChange(id, state) { if (state && !state.ack) { if (id.endsWith("control.createSchedule") && state.val) { const energyDemand = await this.getStateAsync("requirements.energyDemand"); const maxLoad = await this.getStateAsync("requirements.maxLoad"); const avgLoad = await this.getStateAsync("requirements.avgLoad"); if (energyDemand && maxLoad && avgLoad) { await this.createSchedule({ requirements: { energyDemand: energyDemand.val, maxLoad: maxLoad.val, avgLoad: avgLoad.val } }); } } } } onUnload(callback) { try { if (this.scheduleCheckInterval) { clearInterval(this.scheduleCheckInterval); } callback(); } catch (e) { callback(); } } } if (require.main !== module) { // Export the constructor in compact mode module.exports = (options) => new EnergySchedule(options); } else { // otherwise start the instance directly new EnergySchedule(); }