NodeRed 空调的自动控制流程
示例:

代码部分:
[
{
"id": "a306321d8f619896",
"type": "function",
"z": "7fbe68d449c823ba",
"name": "仅当空调非off/unavailable",
"func": "const s = (msg.ac_state || '').toLowerCase();\nif (s && s !== 'off' && s !== 'unavailable') { return msg; }\nreturn null;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 510,
"y": 1820,
"wires": [
[
"2c1dcce70cf77a14"
]
]
},
{
"id": "2c1dcce70cf77a14",
"type": "api-current-state",
"z": "7fbe68d449c823ba",
"name": "读取室内温度",
"server": "ha_server_main_v3",
"version": 3,
"outputs": 1,
"halt_if": "",
"halt_if_type": "str",
"halt_if_compare": "is",
"entity_id": "{{indoor_sensor}}",
"state_type": "str",
"blockInputOverrides": true,
"outputProperties": [
{
"property": "indoor_temp",
"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": 740,
"y": 1820,
"wires": [
[
"ef0fa8a2c073dfe1"
]
]
},
{
"id": "ce975bfaf698dadd",
"type": "api-current-state",
"z": "7fbe68d449c823ba",
"name": "读取空调状态",
"server": "ha_server_main_v3",
"version": 3,
"outputs": 1,
"halt_if": "",
"halt_if_type": "str",
"halt_if_compare": "is",
"entity_id": "{{entity_id}}",
"state_type": "str",
"blockInputOverrides": true,
"outputProperties": [
{
"property": "ac_state",
"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": 1000,
"y": 1720,
"wires": [
[
"a306321d8f619896"
]
]
},
{
"id": "ef0fa8a2c073dfe1",
"type": "api-current-state",
"z": "7fbe68d449c823ba",
"name": "读取室外温度",
"server": "ha_server_main_v3",
"version": 3,
"outputs": 1,
"halt_if": "",
"halt_if_type": "str",
"halt_if_compare": "is",
"entity_id": "{{outdoor_sensor}}",
"state_type": "str",
"blockInputOverrides": true,
"outputProperties": [
{
"property": "outdoor_temp",
"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": 920,
"y": 1820,
"wires": [
[
"5213d005156a5ea8"
]
]
},
{
"id": "4920e5c128c47e62",
"type": "change",
"z": "7fbe68d449c823ba",
"name": "配置实体传感器(按需修改室内传感器ID)",
"rules": [
{
"t": "set",
"p": "entity_id",
"pt": "msg",
"to": "topic",
"tot": "msg"
},
{
"t": "set",
"p": "indoor_sensor",
"pt": "msg",
"to": "sensor.temperature_sensor_4",
"tot": "str"
},
{
"t": "set",
"p": "outdoor_sensor",
"pt": "msg",
"to": "sensor.temperature_sensor_5",
"tot": "str"
},
{
"t": "set",
"p": "season_entity",
"pt": "msg",
"to": "input_select.season",
"tot": "str"
},
{
"t": "set",
"p": "season",
"pt": "msg",
"to": "summer",
"tot": "str"
}
],
"x": 740,
"y": 1720,
"wires": [
[
"ce975bfaf698dadd"
]
]
},
{
"id": "5213d005156a5ea8",
"type": "api-current-state",
"z": "7fbe68d449c823ba",
"name": "读取季节(可选)",
"server": "ha_server_main_v3",
"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": "season_state",
"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": 1100,
"y": 1820,
"wires": [
[
"b4db967e749cf2d3"
]
]
},
{
"id": "f31de8aa367ac4c8",
"type": "api-current-state",
"z": "7fbe68d449c823ba",
"name": "家里是否有人",
"server": "6d42cb4e16b94937",
"version": 3,
"outputs": 2,
"halt_if": "on",
"halt_if_type": "str",
"halt_if_compare": "is",
"entity_id": "input_boolean.person_maybe_left",
"state_type": "str",
"blockInputOverrides": true,
"outputProperties": [
{
"property": "payload",
"propertyType": "msg",
"value": "",
"valueType": "entityState"
},
{
"property": "data",
"propertyType": "msg",
"value": "",
"valueType": "entity"
}
],
"for": "0",
"forType": "num",
"forUnits": "minutes",
"override_topic": false,
"state_location": "payload",
"override_payload": "msg",
"entity_location": "data",
"override_data": "msg",
"x": 480,
"y": 1720,
"wires": [
[
"4920e5c128c47e62"
],
[]
]
},
{
"id": "b4db967e749cf2d3",
"type": "function",
"z": "7fbe68d449c823ba",
"name": "舒适策略 + 季节约束",
"func": "const entity_id = msg.entity_id;\nconst ti = parseFloat(msg.indoor_temp);\nconst to = parseFloat(msg.outdoor_temp);\nlet season = (msg.season_state || msg.season || '').toString().toLowerCase();\nif (!season) season = 'transition'; // 默认过渡季\n\nif (!entity_id || isNaN(ti) || isNaN(to)) {\n node.warn('缺少必要数据: entity_id/indoor_temp/outdoor_temp');\n return null;\n}\n\n// 动态舒适带\nlet comfortLow = 22;\nlet comfortHigh = 26;\nif (to >= 32) comfortHigh -= 1;\nif (to <= 10) comfortLow += 1;\n\nconst delta = ti - to;\nlet actions = [];\nconst set = (action, data) => actions.push({ action, data });\n\n// 先依据室内+室外生成建议动作\nif (ti >= comfortHigh + 2) {\n set('climate.set_hvac_mode', { entity_id, hvac_mode: 'cool' });\n const target = (to >= 32 || delta <= 1) ? 25 : 26;\n const fan = (to >= 32 || delta <= 1) ? 'high' : 'medium';\n set('climate.set_temperature', { entity_id, temperature: target });\n set('climate.set_fan_mode', { entity_id, fan_mode: fan });\n} else if (ti >= comfortHigh) {\n set('climate.set_hvac_mode', { entity_id, hvac_mode: 'cool' });\n const target = (to >= 30 || delta <= 1) ? 26 : 27;\n const fan = (to >= 30 || delta <= 1) ? 'medium' : 'low';\n set('climate.set_temperature', { entity_id, temperature: target });\n set('climate.set_fan_mode', { entity_id, fan_mode: fan });\n} else if (ti > comfortLow && ti < comfortHigh) {\n // 舒适区:按季节强制模式\n if (season === 'summer') {\n set('climate.set_hvac_mode', { entity_id, hvac_mode: 'cool' });\n } else if (season === 'winter') {\n set('climate.set_hvac_mode', { entity_id, hvac_mode: 'heat' });\n } // transition 不强制改模式\n set('climate.set_temperature', { entity_id, temperature: Math.round((comfortLow + comfortHigh) / 2) });\n set('climate.set_fan_mode', { entity_id, fan_mode: 'low' });\n} else if (ti <= comfortLow - 2) {\n set('climate.set_hvac_mode', { entity_id, hvac_mode: 'heat' });\n const target = (to <= 10 || delta >= 1) ? 24 : 23;\n const fan = (to <= 10 || delta >= 3) ? 'high' : 'medium';\n set('climate.set_temperature', { entity_id, temperature: target });\n set('climate.set_fan_mode', { entity_id, fan_mode: fan });\n} else {\n set('climate.set_hvac_mode', { entity_id, hvac_mode: 'heat' });\n const target = (to <= 12 || delta >= 1) ? 23 : 22;\n const fan = (to <= 12 || delta >= 2) ? 'medium' : 'low';\n set('climate.set_temperature', { entity_id, temperature: target });\n set('climate.set_fan_mode', { entity_id, fan_mode: fan });\n}\n\n// 再应用季节白名单过滤\nconst allow = {\n summer: { heat: false, cool: true },\n winter: { heat: true, cool: false },\n transition: { heat: true, cool: true }\n}[season] || { heat: true, cool: true };\n\nactions = actions.filter(act => {\n if (act.action === 'climate.set_hvac_mode') {\n const mode = (act.data && act.data.hvac_mode) || '';\n if (mode === 'heat' && !allow.heat) return false;\n if (mode === 'cool' && !allow.cool) return false;\n }\n return true;\n});\n\nmsg.payload = actions;\nnode.warn(JSON.stringify({ entity_id, ti, to, season, comfortLow, comfortHigh, delta, actions }));\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 600,
"y": 1920,
"wires": [
[
"714e564a60023fb3",
"b5ac70e5b9f6ce9a"
]
]
},
{
"id": "07101e08a0854e19",
"type": "api-current-state",
"z": "7fbe68d449c823ba",
"name": "读取空调状态",
"server": "ha_server_main_v3",
"version": 3,
"outputs": 1,
"halt_if": "",
"halt_if_type": "str",
"halt_if_compare": "is",
"entity_id": "climate.thermostat_5_2",
"state_type": "str",
"blockInputOverrides": true,
"outputProperties": [
{
"property": "ac_state",
"propertyType": "msg",
"value": "",
"valueType": "entityState"
},
{
"property": "topic",
"propertyType": "msg",
"value": "",
"valueType": "triggerId"
}
],
"for": "0",
"forType": "num",
"forUnits": "minutes",
"override_topic": false,
"state_location": "payload",
"override_payload": "msg",
"entity_location": "data",
"override_data": "msg",
"x": 280,
"y": 1720,
"wires": [
[
"f31de8aa367ac4c8"
]
]
},
{
"id": "714e564a60023fb3",
"type": "debug",
"z": "7fbe68d449c823ba",
"name": "预览动作",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 800,
"y": 1960,
"wires": []
},
{
"id": "154b7612694862d8",
"type": "server-state-changed",
"z": "7fbe68d449c823ba",
"name": "儿童房空调",
"server": "6d42cb4e16b94937",
"version": 6,
"outputs": 1,
"exposeAsEntityConfig": "",
"entities": {
"entity": [
"climate.thermostat_5_2"
],
"substring": [],
"regex": []
},
"outputInitially": true,
"stateType": "str",
"ifState": "",
"ifStateType": "str",
"ifStateOperator": "is",
"outputOnlyOnStateChange": true,
"for": "0",
"forType": "num",
"forUnits": "minutes",
"ignorePrevStateNull": false,
"ignorePrevStateUnknown": false,
"ignorePrevStateUnavailable": false,
"ignoreCurrentStateUnknown": false,
"ignoreCurrentStateUnavailable": false,
"outputProperties": [
{
"property": "ac_state",
"propertyType": "msg",
"value": "",
"valueType": "entityState"
},
{
"property": "data",
"propertyType": "msg",
"value": "",
"valueType": "eventData"
},
{
"property": "topic",
"propertyType": "msg",
"value": "",
"valueType": "triggerId"
}
],
"x": 80,
"y": 1720,
"wires": [
[
"07101e08a0854e19"
]
]
},
{
"id": "b5ac70e5b9f6ce9a",
"type": "split",
"z": "7fbe68d449c823ba",
"name": "拆分每个指令",
"splt": "\n",
"spltType": "str",
"arraySplt": 1,
"arraySpltType": "len",
"stream": false,
"addname": "",
"property": "payload",
"x": 820,
"y": 1920,
"wires": [
[
"916c541bccdb25b5"
]
]
},
{
"id": "916c541bccdb25b5",
"type": "api-call-service",
"z": "7fbe68d449c823ba",
"name": "统一调用服务",
"server": "ha_server_main_v3",
"version": 7,
"debugenabled": false,
"action": "",
"floorId": [],
"areaId": [],
"deviceId": [],
"entityId": [],
"labelId": [],
"data": "",
"dataType": "json",
"mergeContext": "",
"mustacheAltTags": false,
"outputProperties": [],
"queue": "none",
"blockInputOverrides": false,
"domain": "",
"service": "",
"x": 1020,
"y": 1920,
"wires": [
[
"5533f5d147383edc"
]
]
},
{
"id": "7d3cb6be0d62ec67",
"type": "mqtt in",
"z": "7fbe68d449c823ba",
"name": "",
"topic": "home/ac_comfort_cycle",
"qos": "2",
"datatype": "auto-detect",
"broker": "b9f7f2abe0e3564e",
"nl": false,
"rap": true,
"rh": 0,
"inputs": 0,
"x": 120,
"y": 1840,
"wires": [
[
"07101e08a0854e19"
]
]
},
{
"id": "10a0aa6bce5e70c4",
"type": "mqtt out",
"z": "7fbe68d449c823ba",
"name": "",
"topic": "home/ac_comfort_trigger",
"qos": "",
"retain": "",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "b9f7f2abe0e3564e",
"x": 1130,
"y": 2040,
"wires": []
},
{
"id": "5533f5d147383edc",
"type": "delay",
"z": "7fbe68d449c823ba",
"name": "节流:15分钟仅1次",
"pauseType": "rate",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "15",
"rateUnits": "second",
"randomFirst": "0",
"randomLast": "0",
"randomUnits": "seconds",
"drop": true,
"allowrate": false,
"outputs": 1,
"x": 1130,
"y": 1980,
"wires": [
[
"10a0aa6bce5e70c4"
]
]
},
{
"id": "mqtt_subscribe",
"type": "mqtt in",
"z": "7fbe68d449c823ba",
"name": "接收循环触发信号",
"topic": "home/ac_comfort_trigger",
"qos": "1",
"datatype": "auto-detect",
"broker": "b9f7f2abe0e3564e",
"nl": false,
"rap": false,
"inputs": 0,
"x": 120,
"y": 2040,
"wires": [
[
"trigger_cycle"
]
]
},
{
"id": "trigger_cycle",
"type": "trigger",
"z": "7fbe68d449c823ba",
"name": "启动15分钟循环",
"op1": "",
"op2": "",
"op1type": "pay",
"op2type": "pay",
"duration": "-15",
"extend": false,
"overrideDelay": true,
"units": "s",
"reset": "reset",
"bytopic": "topic",
"topic": "cycle_trigger",
"outputs": 1,
"x": 320,
"y": 2040,
"wires": [
[
"a55f3374864ca08b"
]
]
},
{
"id": "mqtt_publish_cycle",
"type": "mqtt out",
"z": "7fbe68d449c823ba",
"name": "发送循环检查信号",
"topic": "home/ac_comfort_cycle",
"qos": "1",
"retain": "false",
"respTopic": "",
"contentType": "",
"userProps": "",
"correl": "",
"expiry": "",
"broker": "b9f7f2abe0e3564e",
"x": 770,
"y": 2040,
"wires": []
},
{
"id": "a55f3374864ca08b",
"type": "delay",
"z": "7fbe68d449c823ba",
"name": "节流:15分钟仅1次",
"pauseType": "rate",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "15",
"rateUnits": "second",
"randomFirst": "0",
"randomLast": "0",
"randomUnits": "seconds",
"drop": true,
"allowrate": false,
"outputs": 1,
"x": 530,
"y": 2040,
"wires": [
[
"mqtt_publish_cycle"
]
]
},
{
"id": "ha_server_main_v3",
"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
},
{
"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
},
{
"id": "b9f7f2abe0e3564e",
"type": "mqtt-broker",
"name": "",
"broker": "192.168.101.50",
"port": 1883,
"clientid": "",
"autoConnect": true,
"usetls": false,
"protocolVersion": 4,
"keepalive": 60,
"cleansession": true,
"autoUnsubscribe": true,
"birthTopic": "",
"birthQos": "0",
"birthRetain": "false",
"birthPayload": "",
"birthMsg": {},
"closeTopic": "",
"closeQos": "0",
"closeRetain": "false",
"closePayload": "",
"closeMsg": {},
"willTopic": "",
"willQos": "0",
"willRetain": "false",
"willPayload": "",
"willMsg": {},
"userProps": "",
"sessionExpiry": ""
}
]重点思路,首先流程由空调状态变化触发.流程结束时触发mqtt out,由另一个流程中的tigger节点来触发循环.
为什么不回接到流程前端触发? 目前在学习整理过程中,可以用单独的定时触发流程+mqtt 协议来广播其他空调,统一循环检查.