1. 项目概述与核心价值

家里装了太阳能光伏板的朋友,估计都琢磨过同一个问题:白天发的电用不完,眼睁睁看着它卖回给电网,价格可能只有自用电价的一半甚至更低;晚上没太阳了,又要从电网买高价电来用。这种“发电时用不完,用电时不够发”的错配,让光伏系统的经济性大打折扣。我自己家装了8千瓦的光伏系统后,就一直在想,能不能让那些“可调节”的大功率电器——比如电热水器、充电桩、蓄热电暖器——只在光伏发电有富余的时候工作,实现真正的“自发自用,余电不上网”?

这就是今天要聊的“基于太阳能发电的智能家居能耗控制”方案的核心思路。它不是什么高深莫测的黑科技,而是一套非常务实、可落地的自动化系统。其核心逻辑很简单: 实时比较“光伏发电功率”和“家庭实时用电功率”,当发电有足够富余时,自动开启预设的大功率电器,消耗掉这部分多余的电能;当发电不足或家庭用电激增时,则自动关闭该电器,避免从电网购电。

我选择的硬件核心是 Shelly EM 这款智能电能监测模块。它自带两个电流互感器(CT钳),可以同时、独立地测量两路电路的实时功率,完美契合我们“一路测全家用电,一路测光伏发电”的需求。软件逻辑部分,则用 Node.js 写一个轻量级的后台服务,定期从 Shelly EM 读取数据,进行计算判断,并通过网络控制一个智能开关(如 Shelly 1)来操作电器。

这套方案特别适合有一定动手能力、希望最大化光伏自发自用率、降低电费的家庭用户或极客。它不依赖复杂的家庭自动化中枢(如 Home Assistant),直接通过简单的脚本实现核心逻辑,稳定且响应迅速。接下来,我将从硬件接线、数据获取、逻辑编写到部署优化的全流程,拆解每一个步骤和背后的原理。

2. 系统架构与硬件选型解析

2.1 整体工作流程设计

在动手之前,我们需要把整个系统的工作流想清楚。一个健壮的自动化系统,其流程必须是清晰且闭环的。

  1. 数据采集 :Shelly EM 通过两个CT钳,分别钳在家庭总进线和光伏逆变器输出线上,以每秒数次的频率测量实时功率(单位:瓦特),并通过Wi-Fi将数据上传至本地网络或Shelly云。
  2. 数据获取与计算 :Node.js 服务以固定时间间隔(例如每60秒)通过HTTP请求,从 Shelly EM 的API接口获取最新的功率数据。计算逻辑为: 可用富余功率 = 光伏发电功率 - 家庭总用电功率
  3. 逻辑判断与执行
    • 如果 可用富余功率 > 目标电器额定功率 + 安全阈值 ,则向智能开关发送“开启”指令。
    • 如果 可用富余功率 < 0 (即家庭用电已超过光伏发电,开始从电网取电),则向智能开关发送“关闭”指令。
    • 如果富余功率介于0和目标电器功率之间,则保持开关现有状态不变。
  4. 设备控制 :智能开关(如 Shelly 1)接收到指令后,控制其继电器吸合或断开,从而接通或切断目标电器的电源。

这个流程的核心在于 “可用富余功率” 的计算。这里有一个关键点:Shelly EM 测量功率时是有方向的。通常我们将“用电”定义为正功率,“发电”定义为负功率。所以,如果光伏板正在发电,其测量值会是一个负数。在我们的计算中,需要将其转换为正数来表示“发电量”。

2.2 硬件清单与选型考量

核心硬件:Shelly EM

  • 功能 :双通道电能监测,支持实时功率、电压、电流、电量统计,Wi-Fi连接,提供本地和云端API。
  • 选型理由 :相比单通道的电能监测模块,双通道的Shelly EM可以同时独立监测两路电流,无需额外设备,接线和逻辑都更简洁。其API文档完善,社区支持好,是完成此项目的性价比之选。
  • 关键配件 :务必确认包装内包含 两个电流互感器(CT钳) 。根据你家总进线和逆变器输出线的电流大小选择合适量程,家庭用户一般选择 50A 100A 的规格足够。我的进线是63A空开,使用50A的CT钳完全没问题,但要注意留有余量。

执行硬件:Wi-Fi智能开关

  • 选项1(推荐) Shelly 1 Shelly 1PM 。1PM自带功率测量,可以额外验证被控电器的实际功耗,但本项目不是必须。选择Shelly系列的好处是生态统一,控制API类似。
  • 选项2 :其他支持HTTP/API控制的智能插座或开关,如Sonoff Basic(需刷Tasmota固件)、TP-Link Kasa系列等。你需要事先查明其远程控制的URL地址。
  • 安全警告 :被控电器功率必须 严格小于 智能开关继电器的额定功率(Shelly 1是16A)。控制电热水器、电暖器等大功率设备前,务必核对电器铭牌上的额定电流和功率。

辅助硬件:

  • 网络 :稳定的2.4GHz Wi-Fi网络。Shelly设备对5GHz支持不佳,务必确保安装位置2.4GHz信号强度良好。
  • 供电 :为Shelly EM和Shelly 1提供220V电源。通常可以从被控电器所在的插座或配电箱取电。
  • 安装工具 :螺丝刀、电工胶布、可能需要的线缆等。

注意:电气安全是第一位的!所有接线操作必须在断开总闸或相应回路空开的前提下进行。如果你对家庭电路不熟悉,强烈建议请专业电工协助完成硬件安装部分。

3. 硬件安装与Shelly EM配置详解

3.1 电路接线与CT钳安装

这是整个项目最具实操性也最需要谨慎的一步。错误的接线可能导致数据错误,甚至设备损坏。

第一步:断电与准备 关闭家庭配电箱的总开关,用验电笔确认无电后再操作。规划好Shelly EM的安装位置,最好靠近配电箱,方便取电和布线。

第二步:为Shelly EM供电 找到一条稳定的220V电源线(可以从配电箱的一个空闲空开下引,或从附近一个常通电的插座引),将其 火线(L) 接入Shelly EM的 L 端子, 零线(N) 接入 N 端子。这一步是给设备本身供电。

第三步:安装CT钳1(监测家庭总用电)

  1. 目标线路 :找到从电网接入你家电表,再进入你家配电箱总开关的那根 火线 。通常是一根较粗的导线。
  2. 安装方法 :将第一个CT钳(对应Shelly EM的 P1 通道)套在这根火线上。CT钳上通常有一个箭头标识, 箭头方向应指向用电负载(即指向你家的配电箱) 。这是保证功率测量符号正确的关键。
  3. 接线 :将CT钳的引出线(两根细线)连接到Shelly EM上标有 P1+ P1- 的端子。通常不分正负,但如果后续数据符号不对,可以交换这两根线。

第四步:安装CT钳2(监测光伏发电)

  1. 目标线路 :找到从光伏逆变器交流输出端接入你家配电箱的那根 火线
  2. 安装方法 :将第二个CT钳(对应 P2 通道)套在这根线上。此处的 箭头方向应指向电网方向(即从逆变器指向配电箱/电网) 。这是因为从逆变器的角度看,它是在向电网/家庭负载“输出”电能。
  3. 接线 :将CT钳引出线接入Shelly EM的 P2+ P2- 端子。

重要概念澄清 :有朋友问CT钳是夹在直流侧(太阳能板到逆变器)还是交流侧(逆变器输出)。 必须夹在交流侧! 因为Shelly EM只能测量交流电参数。我们监测的是逆变器之后、送入家庭电网的“可用交流电能”。

第五步:安装执���器Shelly 1 将Shelly 1串联到你要控制的电器(如电暖器)的供电回路中。即:墙壁插座的火线 -> Shelly 1的 L 端子(供电入);Shelly 1的 O 端子(开关出) -> 电暖器插头的火线;零线直接贯通。为Shelly 1本身供电的 N 端子也需要接上零线。确保电暖器本身的物理开关处于打开状态,其通断将由Shelly 1的继电器控制。

3.2 Shelly设备网络配置与数据验证

所有硬件接好后,闭合总开关上电。

  1. 手机App配置 :下载“Shelly”官方App。按照提示,将Shelly EM和Shelly 1分别接入你的Wi-Fi网络。建议为它们设置静态IP或在路由器中做DHCP保留,方便Node.js服务稳定访问。
  2. 验证数据方向 :打开Shelly App,查看Shelly EM的设备页面。你应该能看到两个通道的实时功率。
    • 理想情况 :当家里有负载用电,光伏不发电时(比如晚上),P1(家庭用电)显示为正功率(如+500W),P2(光伏)显示为0或接近0。
    • 当光伏发电且发电量大于家庭用电时,P1可能显示一个较小的正数(家庭基础用电),而 P2应显示为一个负数(如-3000W) 。这个 负数 正是我们需要的“光伏发电功率”。
    • 如果符号相反 :如果P2显示为正数,说明CT钳2的箭头方向反了或者接线反了。最安全的校正方法是 在断电后,将CT钳2的物理方向旋转180度 (即箭头指向反方向),这比交换接线端子更可靠。
  3. 记录关键参数 :在Shelly App中,进入Shelly EM的设备设置,找到“设备信息”,记录下:
    • 设备ID :一串字母数字组合,如 AABBCCDDEEFF
    • 设备通道 :通常是 0
    • 云端访问密钥 :在App的“用户设置”里,可以生成一个“API密钥”(Auth Key)。同时注意你的 服务器地址 ,如果你使用Shelly云服务,通常是 https://shelly-XX-eu.shelly.cloud (XX代表地区编号)。 更推荐使用本地局域网API ,地址为 http://[Shelly-EM的本地IP] ,这样速度更快,不依赖外网。

4. Node.js控制逻辑实现与代码精讲

硬件就绪后,我们开始编写“大脑”——Node.js脚本。我将用一个增强版的脚本来演示,它包含错误处理、状态防抖和本地API调用。

4.1 项目初始化与依赖

首先,确保你的电脑或服务器(比如树莓派)上安装了Node.js。新建一个项目目录并初始化:

mkdir solar-auto-control && cd solar-auto-control
npm init -y

我们主要使用 node-fetch 库来发起HTTP请求。安装它:

npm install node-fetch

4.2 核心控制脚本详解

创建一个名为 solar-controller.js 的文件。下面我逐段解释代码:

// 引入 fetch 库,用于调用API
import fetch from 'node-fetch';

// ============ 配置区域 ============
// 方式一:使用Shelly本地API(推荐,速度快,不依赖互联网)
const SHELLY_EM_IP = '192.168.1.100'; // 替换为你的Shelly EM本地IP
const SHELLY_EM_URL = `http://${SHELLY_EM_IP}/rpc`;

// 方式二:使用Shelly云端API(备用,需要外网)
// const SHELLY_CLOUD_SERVER = 'https://shelly-XX-eu.shelly.cloud';
// const SHELLY_AUTH_KEY = '你的云端AuthKey';

const SHELLY_EM_ID = '你的设备ID'; // 从App中获取
const SHELLY_EM_CHANNEL = 0; // 通常是0

// 智能开关的控制URL(以Shelly 1为例)
const SWITCH_ON_URL = 'http://192.168.1.101/relay/0?turn=on'; // 替换为你的Shelly 1 IP
const SWITCH_OFF_URL = 'http://192.168.1.101/relay/0?turn=off';

// 被控电器的额定功率(瓦特),务必填写准确,并考虑启动电流
const DEVICE_RATED_POWER = 2000; // 例如:2000W 的电暖器
// 动作迟滞阈值,防止在临界点频繁开关
const HYSTERESIS_THRESHOLD = 100; // 单位:瓦特

// 检查间隔(毫秒)
const CHECK_INTERVAL_MS = 60 * 1000; // 60秒

// 全局状态记录,用于防抖和日志
let lastSwitchState = 'unknown'; // 'on', 'off', 'unknown'
// ============ 配置结束 ============

配置要点解析

  • 本地API vs 云端API :优先使用本地IP访问。你可以在路由器管理页面或Shelly App中查看设备的IP地址。本地访问延迟极低(毫秒级),且断网也能工作。
  • 设备功率 DEVICE_RATED_POWER 务必填写电器铭牌上的 额定功率 。对于电热类设备,运行功率通常很稳定。对于电机类(如空调压缩机),启动功率可能数倍于额定功率,本项目逻辑可能不适用,需特别谨慎。
  • 迟滞阈值 HYSTERESIS_THRESHOLD 非常重要。假设没有迟滞,当富余功率在2000W上下轻微波动时,开关会在一分钟内反复动作。设置一个100W的迟滞,意味着:开启条件变为 富余功率 > (2000 + 100)W ,关闭条件变为 富余功率 < 0W 。这样就形成了一个“缓冲区”,避免了频繁开关,保护电器和继电器。
/**
 * 控制智能开关的函数
 * @param {string} action - 'on' 或 'off'
 */
async function controlSwitch(action) {
    const url = action === 'on' ? SWITCH_ON_URL : SWITCH_OFF_URL;
    try {
        const response = await fetch(url);
        const text = await response.text();
        console.log(`[${new Date().toLocaleTimeString()}] 开关 ${action} 指令已发送。响应: ${text}`);
        lastSwitchState = action;
    } catch (error) {
        console.error(`[${new Date().toLocaleTimeString()}] 控制开关时出错:`, error.message);
    }
}

/**
 * 从Shelly EM获取功率数据(使用本地RPC API)
 */
async function getPowerDataFromLocal() {
    try {
        // Shelly EM的RPC调用格式
        const requestBody = {
            id: 1,
            method: "EM.GetStatus",
            params: { id: SHELLY_EM_CHANNEL }
        };

        const response = await fetch(SHELLY_EM_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(requestBody)
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        const data = await response.json();
        // Shelly EM返回的数据结构
        const emeters = data.result.emeters;
        // 通道0:总用电(通常为正)
        const homeConsumption = emeters[0].power; // 单位:瓦特
        // 通道1:光伏发电(发电时为负值)
        const solarProduction = -emeters[1].power; // 转换为正数表示发电量

        return {
            home: homeConsumption,
            solar: solarProduction,
            success: true
        };
    } catch (error) {
        console.error(`[${new Date().toLocaleTimeString()}] 从Shelly EM获取数据失败:`, error.message);
        return { home: 0, solar: 0, success: false };
    }
}

/**
 * 核心决策逻辑
 */
async function checkAndControl() {
    console.log(`\n[${new Date().toLocaleTimeString()}] 开始执行检查...`);

    const powerData = await getPowerDataFromLocal();

    if (!powerData.success) {
        console.log('  数据获取失败,本次跳过。');
        return;
    }

    const { home, solar } = powerData;
    const availableSurplus = solar - home; // 可用富余功率

    console.log(`  家庭实时用电: ${home.toFixed(1)} W`);
    console.log(`  光伏实时发电: ${solar.toFixed(1)} W`);
    console.log(`  可用富余功率: ${availableSurplus.toFixed(1)} W`);
    console.log(`  电器额定功率: ${DEVICE_RATED_POWER} W (迟滞阈值: ${HYSTERESIS_THRESHOLD} W)`);
    console.log(`  开关上次状态: ${lastSwitchState}`);

    // 决策逻辑
    if (availableSurplus < 0) {
        // 情况1:用电大于发电,正在从电网买电
        console.log(`  决策: 可用功率为负,正在使用电网电。`);
        if (lastSwitchState !== 'off') {
            console.log(`  执行: 关闭电器。`);
            await controlSwitch('off');
        } else {
            console.log(`  执行: 状态未变(已关闭),无需操作。`);
        }
    } else if (availableSurplus > (DEVICE_RATED_POWER + HYSTERESIS_THRESHOLD)) {
        // 情况2:富余功率足够大(考虑了迟滞),可以开启电器
        console.log(`  决策: 富余功率超过阈值,可以开启电器。`);
        if (lastSwitchState !== 'on') {
            console.log(`  执行: 开启电器。`);
            await controlSwitch('on');
        } else {
            console.log(`  执行: 状态未变(已开启),无需操作。`);
        }
    } else {
        // 情况3:富余功率为正但不足以开启电器,或处于迟滞区间,保持现状
        console.log(`  决策: 富余功率不足或处于稳定区间,保持当前状态。`);
    }
}

/**
 * 主函数:设置定时器
 */
function main() {
    console.log('=== 太阳能智能控制器已启动 ===');
    console.log(`配置:检查间隔 ${CHECK_INTERVAL_MS / 1000} 秒,电器功率 ${DEVICE_RATED_POWER}W`);
    // 立即执行一次
    checkAndControl();
    // 设置定时循环
    setInterval(checkAndControl, CHECK_INTERVAL_MS);
}

// 启动程序
main();

4.3 脚本运行与后台常驻

保存好脚本后,在终端运行:

node solar-controller.js

你应该能看到控制台每60秒输出一次当前的功率数据和决策日志。测试时,你可以用手遮挡部分光伏板模拟发电量下降,或者打开一个大型电器模拟用电上升,观察脚本是否能正确发出开关指令。

要让这个脚本在服务器(如树莓派)上 24小时不间断运行 ,有几种方法:

  • 使用 screen tmux :在终端会话中启动脚本,然后断开连接,脚本会在后台运行。
  • 使用 pm2 进程管理器 (推荐):
    npm install -g pm2
    pm2 start solar-controller.js --name solar-control
    pm2 save
    pm2 startup # 设置开机自启(根据提示操作)
    
    pm2 可以管理进程状态、查看日志、监控资源,是生产环境更可靠的选择。
  • 创建系统服务 :在树莓派上可以创建一个 systemd 服务单元文件,实现更专业的开机自启和守护。

5. 高级优化与常见问题排查

基础功能实现后,我们可以从稳定性、准确性和用户体验方面进行优化。

5.1 逻辑优化策略

  1. 多时段策略 :电暖器在深夜开启可能毫无意义(即使有光伏余电,但家庭已入睡,无需供暖)。可以在脚本中集成时间判断,例如只在上午8点到晚上10点之间执行自动化逻辑。

    const currentHour = new Date().getHours();
    const ENABLE_HOUR_START = 8;
    const ENABLE_HOUR_END = 22;
    if (currentHour < ENABLE_HOUR_START || currentHour >= ENABLE_HOUR_END) {
        console.log('  非启用时段,强制关闭电器。');
        await controlSwitch('off');
        return;
    }
    
  2. 平滑滤波与数据有效性检查 :Shelly EM的瞬时功率可能有小幅跳动。可以改为读取过去1分钟的平均功率,或者连续读取3次数据取中位数,避免单次波动误触发。

    async function getStablePowerData(sampleCount = 3, intervalMs = 2000) {
        let readings = [];
        for (let i = 0; i < sampleCount; i++) {
            const data = await getPowerDataFromLocal();
            if (data.success) {
                readings.push(data);
            }
            if (i < sampleCount - 1) await delay(intervalMs);
        }
        // 计算中位数或平均值
        const avgHome = readings.reduce((sum, d) => sum + d.home, 0) / readings.length;
        const avgSolar = readings.reduce((sum, d) => sum + d.solar, 0) / readings.length;
        return { home: avgHome, solar: avgSolar, success: readings.length > 0 };
    }
    
  3. 状态持久化与容错 :脚本重启后, lastSwitchState 会丢失。可以每次操作后,将状态写入一个本地文件或小型数据库(如SQLite)。重启时先读取文件,并向智能开关发送一次状态查询请求,同步实际状态,避免误操作。

5.2 常见问题与排查清单

在部署过程中,你可能会遇到以下问题。这里提供一个排查思路:

问题现象 可能原因 排查步骤
Node.js脚本无法获取数据 1. IP地址错误
2. 网络不通
3. Shelly EM未启用本地RPC
1. 在浏览器访问 http://[Shelly-EM-IP]/status ,看能否返回JSON数据。
2. 在Shelly App中确认设备在线。
3. 确保Shelly固件已更新,本地HTTP/RPC访问已开启(默认是开启的)。
功率数据符号错误(如光伏显示为正) CT钳安装方向错误 1. 在光伏发电时,检查Shelly App中P2通道数值。应为负数。
2. 如果为正数, 断电后 将CT钳2的物理方向旋转180度重装。
开关不动作或动作相反 1. 控制URL错误
2. 继电器逻辑相反
1. 手动在浏览器访问 SWITCH_ON_URL SWITCH_OFF_URL ,测试开关是否能正常响应。
2. 检查Shelly 1的设置,确认继电器模式是“开关”模式而非“瞬间”模式。
电器频繁开关 1. 功率阈值设置不当
2. 功率数据波动大
1. 增大 HYSTERESIS_THRESHOLD 迟滞阈值,如从100W调到200W。
2. 实现上述的“平滑滤波”逻辑,使用平均功率而非瞬时功率做判断。
夜间或阴天电器误开启 逻辑缺陷,未考虑发电量为零的情况 在决策逻辑中增加条件: if (solarProduction < 50) { // 假设小于50W视为无有效发电,直接关闭 }
脚本运行一段时间后停止 1. 未捕获的异常导致进程退出
2. 内存泄漏(少见)
1. 用 pm2 管理,它会自动重启崩溃的进程。
2. 在 setInterval fetch 外围添加 try...catch ,确保单个循环错误不会导致整个脚本停止。

5.3 方案扩展思路

这个基础框架可以衍生出很多有趣的玩法:

  • 多设备优先级控制 :如果你有电热水器、电动汽车充电桩、游泳池水泵等多个可调负载,可以给它们设定不同的功率等级和优先级。当富余功率达到2000W时开启热水器,达到6000W时再开启充电桩。
  • 集成到家庭自动化平台 :将Shelly EM的数据通过MQTT协议接入 Home Assistant Node-RED 。在这些可视化平台上,你可以更灵活地设计自动化流程,创建漂亮的能源仪表盘,并与其他智能设备联动(如晴天自动打开窗帘)。
  • 增加储能电池逻辑 :如果你家有储能电池,逻辑可以更复杂:优先用光伏给电池充电,电池充满后再用富余电力启动电器;在电价高峰时段,优先使用电池供电,并关闭非必要大功率电器。
  • 数据记录与分析 :让Node.js脚本将每次读取的功率数据和开关动作记录到数据库(如InfluxDB),然后用Grafana绘制成图表。你可以清晰地看到光伏发电曲线、家庭用电曲线以及自动化控制的效果,为后续优化提供数据支持。

这个项目的魅力在于,它用相对低的成本和一定的动手能力,解决了家庭能源管理中的一个真问题。看到电暖器在阳光明媚的午后自动启动,消耗着“免费”的太阳能,而在阴天或夜晚自动关闭,那种“物尽其用”的满足感,是单纯购买成品设备无法比拟的。

更多推荐