UNPKG

homebridge-solar-monitor-ivk

Version:

Solar production monitor for Homebridge with Pushover notifications

346 lines (289 loc) • 11.3 kB
const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); let Service, Characteristic; /** * Homebridge Solar Monitor Plugin * * PROJECT RULE: NEVER USE HARDCODED DATA * - Always scrape real-time data from e-SenZ website * - Plugin calls solar_scraper.py every 15 minutes * - All data comes from actual e-SenZ login and scraping * - No dummy/test values in production */ class SolarMonitorAccessory { constructor(log, config, api) { this.log = log; this.config = config; this.api = api; this.Service = api.hap.Service; this.Characteristic = api.hap.Characteristic; // Configuration this.username = config.username || '314'; this.password = config.password || 'solgen314'; this.updateInterval = config.updateInterval || 15; // minutes this.pushoverUserKey = config.pushoverUserKey; this.pushoverAppToken = config.pushoverAppToken; this.secondUserKey = config.secondUserKey; // State this.currentPower = 0; this.todayEnergy = 0; this.yesterdayEnergy = 0; this.monthlyEnergy = 0; this.lastUpdate = new Date(); // Create services this.createServices(); // Start monitoring this.startMonitoring(); } createServices() { // Main Solar Monitor Service this.solarService = new this.Service.Lightbulb('Solar Monitor', 'solar-monitor'); // Current Power Characteristic (as brightness) this.currentPowerChar = this.solarService.getCharacteristic(this.Characteristic.Brightness); this.currentPowerChar .on('get', this.getCurrentPower.bind(this)) .on('set', this.setCurrentPower.bind(this)); // On/Off Characteristic (represents if solar is active) this.onChar = this.solarService.getCharacteristic(this.Characteristic.On); this.onChar .on('get', this.getOn.bind(this)) .on('set', this.setOn.bind(this)); // Temperature Service (for Today's Energy) this.todayEnergyService = new this.Service.TemperatureSensor('Today Energy', 'today-energy'); this.todayEnergyChar = this.todayEnergyService.getCharacteristic(this.Characteristic.CurrentTemperature); this.todayEnergyChar.on('get', this.getTodayEnergy.bind(this)); // Humidity Service (for Yesterday's Energy) this.yesterdayEnergyService = new this.Service.HumiditySensor('Yesterday Energy', 'yesterday-energy'); this.yesterdayEnergyChar = this.yesterdayEnergyService.getCharacteristic(this.Characteristic.CurrentRelativeHumidity); this.yesterdayEnergyChar.on('get', this.getYesterdayEnergy.bind(this)); // Contact Sensor (for Monthly Energy) this.monthlyEnergyService = new this.Service.ContactSensor('Monthly Energy', 'monthly-energy'); this.monthlyEnergyChar = this.monthlyEnergyService.getCharacteristic(this.Characteristic.ContactSensorState); this.monthlyEnergyChar.on('get', this.getMonthlyEnergy.bind(this)); } getServices() { return [ this.solarService, this.todayEnergyService, this.yesterdayEnergyService, this.monthlyEnergyService ]; } // Characteristic getters getCurrentPower(callback) { callback(null, this.currentPower); } getOn(callback) { // Solar is "on" if current power > 0 callback(null, this.currentPower > 0); } getTodayEnergy(callback) { // Convert kWh to temperature-like value (multiply by 10 for better visibility) callback(null, this.todayEnergy * 10); } getYesterdayEnergy(callback) { // Convert kWh to humidity-like value (multiply by 10 for better visibility) callback(null, Math.min(this.yesterdayEnergy * 10, 100)); } getMonthlyEnergy(callback) { // Contact sensor: DETECTED if monthly energy > 0 callback(null, this.monthlyEnergy > 0 ? this.Characteristic.ContactSensorState.CONTACT_DETECTED : this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED); } // Characteristic setters (read-only, so just return success) setCurrentPower(value, callback) { this.currentPower = value; callback(null); } setOn(value, callback) { callback(null); } // Start monitoring startMonitoring() { this.log('Starting solar monitoring...'); // Run initial scrape this.scrapeData(); // Set up interval setInterval(() => { this.scrapeData(); }, this.updateInterval * 60 * 1000); } // Check if we're in solar hours (6 AM - 6:30 PM IST) isSolarHours() { const now = new Date(); // RPi is already in IST timezone, so no need to add 5.5 hours const hour = now.getHours(); const minute = now.getMinutes(); const currentTime = hour + minute / 60; // Solar hours: 6:00 AM to 6:30 PM IST const isSolar = currentTime >= 6 && currentTime <= 18.5; this.log(`Current time: ${hour}:${minute.toString().padStart(2, '0')} IST (${currentTime.toFixed(2)}), Solar hours: ${isSolar}`); return isSolar; } // Scrape REAL solar data from e-SenZ website // PROJECT RULE: NEVER USE HARDCODED DATA - ALWAYS SCRAPE REAL DATA async scrapeData() { try { // Check if we're in solar hours if (!this.isSolarHours()) { this.log('Outside solar hours (6 AM - 6:30 PM IST), skipping scrape'); return; } this.log('Scraping REAL solar data from e-SenZ website...'); // Run Python scraper to get REAL data from e-SenZ const pythonScript = path.join(__dirname, 'solar_scraper.py'); const result = await this.runPythonScript(pythonScript, this.username, this.password); if (result.success) { this.log('āœ… REAL data obtained from e-SenZ website'); this.updateCharacteristics(result.data); this.sendNotification(result.data); } else { this.log.error('Failed to scrape REAL data from e-SenZ:', result.error); } } catch (error) { this.log.error('Error scraping REAL data from e-SenZ:', error); } } // Run Python script runPythonScript(scriptPath, username, password) { return new Promise((resolve) => { const pythonProcess = spawn('python3', [scriptPath, username, password]); let output = ''; let error = ''; pythonProcess.stdout.on('data', (data) => { output += data.toString(); }); pythonProcess.stderr.on('data', (data) => { error += data.toString(); }); pythonProcess.on('close', (code) => { if (code === 0) { try { // Parse the JSON output from the script const lines = output.split('\n'); for (let i = lines.length - 1; i >= 0; i--) { const line = lines[i].trim(); if (line.startsWith('{') && line.endsWith('}')) { const data = JSON.parse(line); if (data.error) { resolve({ success: false, error: data.error }); } else { resolve({ success: true, data }); } return; } } resolve({ success: false, error: 'No JSON data found' }); } catch (e) { resolve({ success: false, error: 'Failed to parse JSON: ' + e.message }); } } else { resolve({ success: false, error: error || 'Script failed with code: ' + code }); } }); }); } // Update HomeKit characteristics updateCharacteristics(data) { // Parse current power (remove "kW" and convert to number) const powerMatch = data.current_power?.match(/(\d+(?:\.\d+)?)/); if (powerMatch) { this.currentPower = parseFloat(powerMatch[1]) * 10; // Scale for brightness } // Parse energy values const todayMatch = data.today_energy?.match(/(\d+(?:\.\d+)?)/); if (todayMatch) { this.todayEnergy = parseFloat(todayMatch[1]); } const yesterdayMatch = data.yesterday_energy?.match(/(\d+(?:\.\d+)?)/); if (yesterdayMatch) { this.yesterdayEnergy = parseFloat(yesterdayMatch[1]); } const monthlyMatch = data.monthly_energy?.match(/(\d+(?:\.\d+)?)/); if (monthlyMatch) { this.monthlyEnergy = parseFloat(monthlyMatch[1]); } // Update characteristics this.currentPowerChar.updateValue(this.currentPower); this.onChar.updateValue(this.currentPower > 0); this.todayEnergyChar.updateValue(this.todayEnergy * 10); this.yesterdayEnergyChar.updateValue(Math.min(this.yesterdayEnergy * 10, 100)); this.monthlyEnergyChar.updateValue(this.monthlyEnergy > 0 ? this.Characteristic.ContactSensorState.CONTACT_DETECTED : this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED); this.lastUpdate = new Date(); this.log(`Updated: Power=${this.currentPower/10}kW, Today=${this.todayEnergy}kWh, Yesterday=${this.yesterdayEnergy}kWh`); } // Send Pushover notification async sendNotification(data) { if (!this.pushoverUserKey || !this.pushoverAppToken) { return; } try { const message = `šŸ”‹ Current Power: ${data.current_power} šŸ“Š E Today: ${data.today_energy} šŸ“ˆ E Yesterday: ${data.yesterday_energy} šŸ“… E This Month: ${data.monthly_energy || '0.00 kWh'} ā° ${new Date().toLocaleString()}`; // Send to primary user await this.sendPushoverNotification(this.pushoverUserKey, this.pushoverAppToken, message); // Send to second user if configured if (this.secondUserKey) { await this.sendPushoverNotification(this.secondUserKey, this.pushoverAppToken, message); } } catch (error) { this.log.error('Failed to send notification:', error); } } // Send individual Pushover notification async sendPushoverNotification(userKey, appToken, message) { const https = require('https'); const querystring = require('querystring'); const postData = querystring.stringify({ token: appToken, user: userKey, message: message, title: '⚔ Solar Production Update', sound: 'cosmic', priority: 0 }); const options = { hostname: 'api.pushover.net', port: 443, path: '/1/messages.json', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }; return new Promise((resolve, reject) => { const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { if (res.statusCode === 200) { this.log(`Pushover notification sent to user ${userKey}`); resolve(); } else { reject(new Error(`Pushover API error: ${res.statusCode} - ${data}`)); } }); }); req.on('error', (error) => { reject(error); }); req.write(postData); req.end(); }); } } module.exports = function(homebridge) { Service = homebridge.hap.Service; Characteristic = homebridge.hap.Characteristic; homebridge.registerAccessory('homebridge-solar-monitor-ivk', 'SolarMonitor', SolarMonitorAccessory); };