homebridge-solar-monitor-ivk
Version:
Solar production monitor for Homebridge with Pushover notifications
346 lines (289 loc) ⢠11.3 kB
JavaScript
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);
};