[ { "id": "6dfa989bb120e6c6", "type": "tab", "label": "Home Presence" }, { "id": "pres_c_homemode", "type": "comment", "z": "6dfa989bb120e6c6", "name": "── Home Mode ──", "wires": [], "info": "", "x": 130, "y": 60 }, { "id": "pres001a2b3c4d5e", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Mischa presence", "wires": [ [ "pres002a2b3c4d5e" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "person.mischa_gorinskat" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 120, "y": 100 }, { "id": "pres002a2b3c4d5e", "type": "switch", "z": "6dfa989bb120e6c6", "name": "Home or Away?", "wires": [ [ "pres003a2b3c4d5e" ], [ "pres004a2b3c4d5e" ] ], "property": "payload", "propertyType": "msg", "rules": [ { "t": "eq", "v": "home", "vt": "str" }, { "t": "eq", "v": "not_home", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 400, "y": 100 }, { "id": "pres003a2b3c4d5e", "type": "api-call-service", "z": "6dfa989bb120e6c6", "name": "Set Home", "wires": [ [] ], "server": "657c7463.48e1e4", "version": 7, "debugenabled": false, "action": "input_select.select_option", "floorId": [], "areaId": [], "deviceId": [], "entityId": [ "input_select.home_mode" ], "labelId": [], "data": "{\"option\": \"Home\"}", "dataType": "json", "mergeContext": "", "mustacheAltTags": false, "outputProperties": [], "queue": "none", "blockInputOverrides": false, "domain": "input_select", "service": "select_option", "x": 660, "y": 60 }, { "id": "pres004a2b3c4d5e", "type": "api-call-service", "z": "6dfa989bb120e6c6", "name": "Set Away", "wires": [ [] ], "server": "657c7463.48e1e4", "version": 7, "debugenabled": false, "action": "input_select.select_option", "floorId": [], "areaId": [], "deviceId": [], "entityId": [ "input_select.home_mode" ], "labelId": [], "data": "{\"option\": \"Away\"}", "dataType": "json", "mergeContext": "", "mustacheAltTags": false, "outputProperties": [], "queue": "none", "blockInputOverrides": false, "domain": "input_select", "service": "select_option", "x": 660, "y": 140 }, { "id": "pres_sm_c_header", "type": "comment", "z": "6dfa989bb120e6c6", "name": "── Room Presence State Machine ──", "wires": [], "info": "", "x": 190, "y": 300 }, { "id": "pres_sm_c_person", "type": "comment", "z": "6dfa989bb120e6c6", "name": "Person", "wires": [], "info": "", "x": 90, "y": 360 }, { "id": "pres_sm_c_hall", "type": "comment", "z": "6dfa989bb120e6c6", "name": "Hallway (direction + state)", "wires": [], "info": "", "x": 150, "y": 480 }, { "id": "pres_sm_c_rooms", "type": "comment", "z": "6dfa989bb120e6c6", "name": "Room Motion Sensors", "wires": [], "info": "", "x": 140, "y": 780 }, { "id": "pres_sm_c_sm", "type": "comment", "z": "6dfa989bb120e6c6", "name": "State Machine", "wires": [], "info": "", "x": 450, "y": 580 }, { "id": "pres_sm_c_out", "type": "comment", "z": "6dfa989bb120e6c6", "name": "Outputs", "wires": [], "info": "", "x": 730, "y": 580 }, { "id": "pres_sm_person", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Person (room SM)", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "person.mischa_gorinskat" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "string", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 130, "y": 400 }, { "id": "pres_sm_hall1", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Hallway S1 (corner/front door)", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.hallway_motion_sensor_1_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 160, "y": 520 }, { "id": "pres_sm_hall2", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Hallway S2 (near living room)", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.hallway_motion_sensor_2_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 160, "y": 580 }, { "id": "pres_sm_hall3", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Hallway S3 (near kitchen)", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.hallway_motion_sensor_3_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 150, "y": 640 }, { "id": "pres_sm_hallg", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Hallway (group)", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.hallway_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 120, "y": 700 }, { "id": "pres_sm_lr", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Living Room Motion", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.livingroom_motion_sensor_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 130, "y": 820 }, { "id": "pres_sm_bed", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Bedroom Motion", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.bedroom_motion_sensor_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 120, "y": 880 }, { "id": "pres_sm_bath", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Bathroom Motion", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.bathroom_motion_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 120, "y": 940 }, { "id": "pres_sm_kit", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Kitchen Motion", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.kitchen_motion_sensor_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 120, "y": 1000 }, { "id": "pres_sm_func", "type": "function", "z": "6dfa989bb120e6c6", "name": "Room Presence SM", "wires": [ [ "pres_sm_api" ], [ "pres_sm_dbg" ] ], "func": "// === ROOM PRESENCE STATE MACHINE ===\n// msg.topic = triggering entity_id (from triggerId output property)\n// msg.payload = entity state string\n// msg.data = full HASC event data\n\nconst entityId = (msg.data && msg.data.entity_id) ? msg.data.entity_id : (msg.topic || '');\nconst newState = (msg.data && msg.data.new_state) ? msg.data.new_state.state : msg.payload;\n\n// Guest mode guard — multi-person breaks single-occupant assumptions\nconst ha = global.get('homeassistant') || {};\nconst haStates = (ha.homeAssistantLatest || {}).states || {};\nconst guestMode = (haStates['input_boolean.guest_mode'] || {}).state;\nif (guestMode === 'on') return null;\n\n// Current state from flow context\nconst currentRoom = flow.get('currentRoom') || 'unknown';\n\n// ── Helpers ────────────────────────────────────────────────────────────────\n\nfunction capitalize(s) {\n const map = {\n unknown: 'Unknown', away: 'Away', hallway: 'Hallway',\n living_room: 'Livingroom', bedroom: 'Bedroom',\n bathroom: 'Bathroom', kitchen: 'Kitchen', balcony: 'Balcony'\n };\n return map[s] || (s.charAt(0).toUpperCase() + s.slice(1));\n}\n\nfunction cancelDestTimer() {\n const h = flow.get('destTimerHandle');\n if (h) { clearTimeout(h); flow.set('destTimerHandle', null); }\n}\n\n\nfunction setDestTimer() {\n cancelDestTimer();\n const h = setTimeout(function () {\n const lr = flow.get('lastRoom') || 'unknown';\n flow.set('currentRoom', lr);\n flow.set('destTimerHandle', null);\n node.send([\n { payload: capitalize(lr) },\n { payload: '[presence] dest-timer expired → reverted to ' + lr }\n ]);\n }, 90000);\n flow.set('destTimerHandle', h);\n}\n\n\nfunction setState(newRoom) {\n const prev = flow.get('currentRoom') || 'unknown';\n if (newRoom === prev) return null; // no change needed\n flow.set('lastRoom', prev);\n flow.set('currentRoom', newRoom);\n return [\n { payload: capitalize(newRoom) },\n { payload: '[presence] ' + prev + ' → ' + newRoom + ' (via ' + entityId + ')' }\n ];\n}\n\nfunction updateHallwaySequence(sensorNum) {\n // TODO: direction detection not yet implemented — sequence data is stored but not consumed.\n // Intended to detect hallway direction (e.g. S1→S2 = heading toward living room,\n // S2→S1 = heading toward front door).\n let seq = flow.get('hallwaySequence') || [];\n const now = Date.now();\n seq = seq.filter(function (e) { return now - e.time < 15000; });\n seq.push({ sensor: sensorNum, time: now });\n flow.set('hallwaySequence', seq);\n flow.set('lastHallwayTime', now);\n}\n\n// ── Transitions ────────────────────────────────────────────────────────────\n\n// 1. Person home / away\nif (entityId === 'person.mischa_gorinskat') {\n if (newState === 'not_home') {\n cancelDestTimer();\n return setState('away');\n }\n if (newState === 'home') {\n return setState('unknown');\n }\n return null;\n}\n\n// Ignore all motion when person is away\nif (currentRoom === 'away') return null;\n\n// 2. Individual hallway sensors — direction tracking only, no state change\nif (entityId === 'binary_sensor.hallway_motion_sensor_1_occupancy') {\n if (newState === 'on') updateHallwaySequence(1);\n return null;\n}\nif (entityId === 'binary_sensor.hallway_motion_sensor_2_occupancy') {\n if (newState === 'on') updateHallwaySequence(2);\n return null;\n}\nif (entityId === 'binary_sensor.hallway_motion_sensor_3_occupancy') {\n if (newState === 'on') updateHallwaySequence(3);\n return null;\n}\n\n// 3. Hallway group — primary state change trigger\nif (entityId === 'binary_sensor.hallway_occupancy') {\n if (newState === 'on') {\n flow.set('lastRoom', currentRoom); // save room before entering hallway\n setDestTimer(); // 90s: if no room fires → revert\n return setState('hallway');\n }\n return null; // hallway turning off doesn't trigger a state change\n}\n\n// 4. Room sensors\nconst roomMap = {\n 'binary_sensor.livingroom_motion_sensor_occupancy': 'living_room',\n 'binary_sensor.bedroom_motion_sensor_occupancy': 'bedroom',\n 'binary_sensor.bathroom_motion_occupancy': 'bathroom',\n 'binary_sensor.kitchen_motion_sensor_occupancy': 'kitchen',\n 'binary_sensor.balcony_motion_occupancy': 'balcony'\n};\n\nconst targetRoom = roomMap[entityId];\nif (targetRoom && newState === 'on') {\n cancelDestTimer(); // confirmed destination\n\n const canTransition =\n currentRoom === 'hallway' ||\n currentRoom === targetRoom ||\n currentRoom === 'unknown' ||\n (targetRoom === 'living_room' && currentRoom === 'balcony') || // return from balcony\n (targetRoom === 'balcony' && currentRoom === 'living_room'); // step onto balcony\n\n if (canTransition) return setState(targetRoom);\n return null;\n}\n\nreturn null;", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 480, "y": 620 }, { "id": "pres_sm_api", "type": "api-call-service", "z": "6dfa989bb120e6c6", "name": "Set Room Presence", "wires": [ [] ], "server": "657c7463.48e1e4", "version": 7, "debugenabled": false, "action": "input_select.select_option", "floorId": [], "areaId": [], "deviceId": [], "entityId": [ "input_select.room_presence" ], "labelId": [], "data": "{\"option\": \"{{{payload}}}\"}", "dataType": "json", "mergeContext": "", "mustacheAltTags": false, "outputProperties": [], "queue": "none", "blockInputOverrides": false, "domain": "input_select", "service": "select_option", "x": 780, "y": 620 }, { "id": "pres_sm_dbg", "type": "debug", "z": "6dfa989bb120e6c6", "name": "Presence log", "wires": [], "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "statusVal": "", "statusType": "auto", "x": 750, "y": 680 }, { "id": "pres_sm_balcony", "type": "server-state-changed", "z": "6dfa989bb120e6c6", "name": "Balcony Motion", "wires": [ [ "pres_sm_func" ] ], "server": "657c7463.48e1e4", "version": 6, "outputs": 1, "exposeAsEntityConfig": "", "entities": { "entity": [ "binary_sensor.balcony_motion_occupancy" ], "substring": [], "regex": [] }, "outputInitially": false, "stateType": "str", "ifState": "", "ifStateType": "str", "ifStateOperator": "is", "outputOnlyOnStateChange": true, "for": 0, "forType": "num", "forUnits": "minutes", "ignorePrevStateNull": false, "ignorePrevStateUnknown": false, "ignorePrevStateUnavailable": false, "ignoreCurrentStateUnknown": true, "ignoreCurrentStateUnavailable": true, "outputProperties": [ { "property": "payload", "propertyType": "msg", "value": "", "valueType": "entityState" }, { "property": "data", "propertyType": "msg", "value": "", "valueType": "eventData" }, { "property": "topic", "propertyType": "msg", "value": "", "valueType": "triggerId" } ], "x": 120, "y": 1060 } ]