[空调控制]NodeRed自动判断当前季节
根据过去7天温度变化数据主动识别判断当前季节.
创建一个辅助元素 input_select.season
可选参数: summer winter transition
NodeRed流程:
[
{
"id": "75c4c5c446cd5b15",
"type": "function",
"z": "63f66746ece01b5f",
"name": "检查是否需要更新",
"func": "// 检查检测到的季节是否与当前季节不同\nconst detectedSeason = msg.detected_season || 'transition';\nconst currentSeason = msg.current_season || 'transition';\n\nif (detectedSeason !== currentSeason) {\n // 季节不同,需要更新\n return msg;\n} else {\n // 季节相同,不需要更新\n node.warn(`季节未变化,跳过更新: ${currentSeason}`);\n return null;\n}",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 700,
"y": 200,
"wires": [
[
"e41988d70742a5b9"
]
]
},
{
"id": "59d25f3c136ea6e4",
"type": "function",
"z": "63f66746ece01b5f",
"name": "分析温度数据判断季节",
"func": "// 解析历史数据\nconst historyData = msg.payload;\nif (!historyData || !Array.isArray(historyData) || historyData.length === 0) {\n node.warn('无法获取历史温度数据');\n return null;\n}\n\n// 提取所有温度值\nconst temps = [];\nfor (const dayData of historyData) {\n if (Array.isArray(dayData)) {\n for (const record of dayData) {\n const temp = parseFloat(record.state);\n if (!isNaN(temp)) {\n temps.push(temp);\n }\n }\n }\n}\n\nif (temps.length === 0) {\n node.warn('历史数据中没有有效的温度值');\n return null;\n}\n\n// 计算统计值\nconst avgTemp = temps.reduce((a, b) => a + b, 0) / temps.length;\nconst maxTemp = Math.max(...temps);\nconst minTemp = Math.min(...temps);\n\n// 计算温度趋势:最近3天 vs 前4天\nconst midPoint = Math.floor(temps.length / 2);\nconst recentTemps = temps.slice(midPoint);\nconst earlierTemps = temps.slice(0, midPoint);\nconst recentAvg = recentTemps.reduce((a, b) => a + b, 0) / recentTemps.length;\nconst earlierAvg = earlierTemps.length > 0 ? earlierTemps.reduce((a, b) => a + b, 0) / earlierTemps.length : recentAvg;\nconst tempTrend = recentAvg - earlierAvg;\n\n// 季节判断逻辑\nlet detectedSeason = 'transition';\n\n// 夏季判断:平均温度高或最高温度高\nif (avgTemp >= 25 || maxTemp >= 30) {\n detectedSeason = 'summer';\n}\n// 冬季判断:平均温度低或最低温度低\nelse if (avgTemp <= 15 || minTemp <= 5) {\n detectedSeason = 'winter';\n}\n// 过渡季节判断:结合趋势\nelse if (avgTemp > 20 && tempTrend > 2) {\n // 温度较高且上升趋势明显 -> 可能进入夏季\n detectedSeason = 'summer';\n}\nelse if (avgTemp < 20 && tempTrend < -2) {\n // 温度较低且下降趋势明显 -> 可能进入冬季\n detectedSeason = 'winter';\n}\nelse {\n // 其他情况保持过渡季\n detectedSeason = 'transition';\n}\n\n// 保存分析结果\nmsg.detected_season = detectedSeason;\nmsg.current_season = msg.current_season || 'transition';\nmsg.stats = {\n avgTemp: avgTemp.toFixed(1),\n maxTemp: maxTemp.toFixed(1),\n minTemp: minTemp.toFixed(1),\n tempTrend: tempTrend.toFixed(1),\n dataPoints: temps.length\n};\n\nnode.warn(`季节判断: 当前=${msg.current_season}, 检测=${detectedSeason}, 平均=${avgTemp.toFixed(1)}°C, 趋势=${tempTrend > 0 ? '+' : ''}${tempTrend.toFixed(1)}°C`);\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 200,
"wires": [
[
"75c4c5c446cd5b15"
]
]
},
{
"id": "e41988d70742a5b9",
"type": "function",
"z": "63f66746ece01b5f",
"name": "构建服务调用数据",
"func": "// 构建服务调用数据\nconst entity_id = msg.season_entity || 'input_select.season';\nconst option = msg.detected_season || 'transition';\n\nmsg.payload = {\n action: \"input_select.select_option\",\n data: {\n entity_id,\n option\n }\n};\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 220,
"y": 320,
"wires": [
[
"a8f72b6f4884b3e4"
]
]
},
{
"id": "3739259c563e17cf",
"type": "http request",
"z": "63f66746ece01b5f",
"name": "获取历史温度数据",
"method": "use",
"ret": "obj",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [],
"x": 220,
"y": 200,
"wires": [
[
"59d25f3c136ea6e4"
]
]
},
{
"id": "a8f72b6f4884b3e4",
"type": "api-call-service",
"z": "63f66746ece01b5f",
"name": "更新季节",
"server": "6d42cb4e16b94937",
"version": 7,
"debugenabled": false,
"action": "",
"floorId": [],
"areaId": [],
"deviceId": [],
"entityId": [],
"labelId": [],
"data": "",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"blockInputOverrides": false,
"domain": "",
"service": "",
"x": 410,
"y": 320,
"wires": [
[
"7c62f2f3ae85e9c7"
]
]
},
{
"id": "906ad1eb70807484",
"type": "function",
"z": "63f66746ece01b5f",
"name": "构建历史数据请求",
"func": "// 计算7天前的时间戳\nconst now = new Date();\nconst sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);\nconst endTime = now.toISOString();\nconst startTime = sevenDaysAgo.toISOString();\n\n// 配置HA地址(根据你的实际情况修改)\n// 方式1: 如果Node-RED和HA在同一服务器,使用localhost\n// 方式2: 如果Node-RED是HA Add-on,可能需要使用supervisor或localhost\n// 方式3: 如果Node-RED在外部,使用完整的HA地址\nconst haBaseUrl = 'http://10.0.0.90:8123'; // 修改为你的HA地址\n// const haBaseUrl = 'http://homeassistant.local:8123'; // 或者使用.local地址\n// const haBaseUrl = 'http://192.168.1.100:8123'; // 或者使用IP地址\n\n// 配置HA长期访问令牌(在HA中:配置 -> 长期访问令牌 -> 创建令牌)\nconst haAccessToken = 'Im6Kbrv4IuWVrtWdxfzGv-szHxUpcsTPI1zwJfM'; // 替换为你的令牌\n\n// 构建HA History API请求URL\nmsg.url = `${haBaseUrl}/api/history/period/${startTime}?filter_entity_id=${msg.outdoor_sensor}&end_time=${endTime}`;\nmsg.method = 'GET';\n\n// 添加认证头\nmsg.headers = {\n 'Authorization': `Bearer ${haAccessToken}`,\n 'Content-Type': 'application/json'\n};\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 780,
"y": 80,
"wires": [
[
"3739259c563e17cf"
]
]
},
{
"id": "7c62f2f3ae85e9c7",
"type": "debug",
"z": "63f66746ece01b5f",
"name": "调试输出",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 670,
"y": 320,
"wires": []
},
{
"id": "6d252af7fe1f2b77",
"type": "api-current-state",
"z": "63f66746ece01b5f",
"name": "读取当前季节",
"server": "6d42cb4e16b94937",
"version": 3,
"outputs": 1,
"halt_if": "",
"halt_if_type": "str",
"halt_if_compare": "is",
"entity_id": "{{season_entity}}",
"state_type": "str",
"blockInputOverrides": true,
"outputProperties": [
{
"property": "current_season",
"propertyType": "msg",
"value": "",
"valueType": "entityState"
}
],
"for": "0",
"forType": "num",
"forUnits": "minutes",
"override_topic": false,
"state_location": "payload",
"override_payload": "msg",
"entity_location": "data",
"override_data": "msg",
"x": 540,
"y": 80,
"wires": [
[
"906ad1eb70807484"
]
]
},
{
"id": "f820c2e8efa41063",
"type": "change",
"z": "63f66746ece01b5f",
"name": "配置传感器",
"rules": [
{
"t": "set",
"p": "outdoor_sensor",
"pt": "msg",
"to": "sensor.xiaomi_cn_blt_3_1lubuvk9t0k00_mini_temperature_p_2_1001",
"tot": "str"
},
{
"t": "set",
"p": "season_entity",
"pt": "msg",
"to": "input_select.season",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 330,
"y": 80,
"wires": [
[
"6d252af7fe1f2b77"
]
]
},
{
"id": "4131351f1f9ee240",
"type": "inject",
"z": "63f66746ece01b5f",
"name": "每天检查一次",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "0 2 * * *",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 130,
"y": 80,
"wires": [
[
"f820c2e8efa41063"
]
]
},
{
"id": "6d42cb4e16b94937",
"type": "server",
"name": "Home Assistant_main",
"version": 5,
"addon": true,
"rejectUnauthorizedCerts": true,
"ha_boolean": "y|yes|true|on|home|open",
"connectionDelay": true,
"cacheJson": true,
"heartbeat": true,
"heartbeatInterval": 30,
"areaSelector": "friendlyName",
"deviceSelector": "friendlyName",
"entitySelector": "friendlyName",
"statusSeparator": ": ",
"statusYear": "hidden",
"statusMonth": "short",
"statusDay": "numeric",
"statusHourCycle": "default",
"statusTimeFormat": "h:m",
"enableGlobalContextStore": true
}
]
