NodeRed 空调的自动控制流程

52

示例:

代码部分:

[
    {
        "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 协议来广播其他空调,统一循环检查.