Using node-red and atvremote to pass "play state" from apple tv to openhab

By request from @allen and @rkrisi - here is my node-red code to connect openhab to my apple TV’s (ATV 4 and ATV 4k).

Step 1 will be to install atvremote and get the codes for your apple tv. Note home sharing needs to be enabled on your apple tv.


Follow instructions to install pyatv then run the following command:

atvremote pair

From here you will go to the “remotes and devices” screen in your apple tv settings and choose “pyatv” then type the 1234 code as provided in the command line.

There will also be a long code like 0x00000000000000 - this is your “login_id” as used below. I’m assuming you know how to find your apple tv IP address - if not go ahead and run atvremote scan to see all apple tvs on your network.

Now you’re ready to move on to node-red. One problem you may come across is you’ll need a way to trigger when this program runs because if it’s running all the time you’ll notice your TV will turn on by itself quite often. I use the Onkyo binding for my home theater - when the Onkyo turns on I start this automation.

My other TV is not as smart - it has a USB power socket that turns on and off with the tv so I’ve connected a 5v reed switch to that USB power and run a wire to my openhab GPIO. Basic but it works.

Once you’ve determined which item will trigger this system to start you’re ready for node-red. Here is an overview, I’ll also share what is in each function.


From left to right we have the following:

First the onkyo ON/OFF state. This will always send a payload of ON, OFF, or NULL. I have additional “on” and “off” inject nodes for debugging purposes.

Next is one of the two “big ones” - the function node. The purpose of this guy is to take ON/OFF and put together the command for the exec node to start watching for atvremote push updates. Looks like this inside:

var zeroMsg = { payload:0 };
if(msg.payload == "ON"){
    msg.payload = "atvremote --address **YOUR_IP** --login_id **YOUR_LOGIN_ID** push_updates";
return [msg,zeroMsg];
}
if(msg.payload == "OFF"){
    msg.kill = 'SIGTERM';
return [msg,zeroMsg];
}

This formats two messages - one I use to reset my “play state” item to 0 when the onkyo turns on or off. The other goes into the exec node and is either the command to begin atvremote or to kill (msg.kill) the process when I turn off the TV.

The exec mode is pretty basic, just takes the commands and runs them in the command line. It’s important to set it to “spawn” so that you get the push updates from the atvremote program. Looks like this:
24%20AM

The output of this guy is the money item in this flow. It goes into a split node first (because the messages come out as multi-line items) to filter by line. Split looks like:

Now the really important part. The function to read through all of the messages coming out of the split node and turn them into something actually useful. I use 3 play states: playing, paused, and stopped/idle. The code for the function is:

if(msg.payload == "Play state: Paused") {
    msg.payload = 2;
return msg;
}
if(msg.payload == "Play state: Playing") {
    msg.payload = 1;
return msg;
}
if(msg.payload == "Play state: Fast backward") {
    msg.payload = 1;
return msg;
}
if(msg.payload == "Play state: Fast forward") {
    msg.payload = 1;
return msg;
}
if(msg.payload == "Play state: Idle") {
    msg.payload = 3;
return msg;
}
if(msg.payload == "Play state: No Media") {
    msg.payload = 3;
return msg;
}
if(msg.payload == "Play state: Loading") {
    msg.payload = 1;
return msg;
}

So I watch the messages coming from the exec spawn (nicely split by new lines) and send out a 1, 2, or 3 based on what’s happening. These are the messages that eventually go on to openhab so that I can run rules like “when play_state received command 1 …”

The 550ms trigger is a housekeeping item. I find that when you start some things (netflix is especially bad) they send a few rapid-fire commands going from pause to play to stop to play to fast backward (and on) very quickly while the video loads. The trigger node prevents this from going on to make a disco dance party in my theater room when I start playing a movie. Looks like:


Finally we have a RBE node (only passes the message if it’s changed from the previous message) and goes into openhab as an “item command” for use in rules.

The 14 minute trigger and “CURL TV off” are bonus commands. I’ve linked into the vizio api to turn off my tv after 14 minutes of a 0, 2, or 3 command coming through (1 cancels because it’ll be playing something new). This way my whole system turns off reliably and prevents my 4-year-old from walking away and leaving Frozen playing the title screen for hours.

Put it all together and now you’ve got the ability for openhab to react to your apple tv play state. I remember when I first asked about this on the forums months ago someone’s response was “why don’t you start using kodi to play your tv?” - clearly that wasn’t good enough for me.

I don’t know how to attach a text file so the next post here will be this whole flow for “easy import” into node-red.

[
    {
        "id": "94004bee.36216",
        "type": "exec",
        "z": "a0d6949c.443578",
        "command": "",
        "addpay": true,
        "append": "",
        "useSpawn": "true",
        "timer": "",
        "oldrc": false,
        "name": "TV Room",
        "x": 400,
        "y": 260,
        "wires": [
            [
                "46398b5c.9040e4"
            ],
            [],
            []
        ]
    },
    {
        "id": "7bed54c9.660c04",
        "type": "function",
        "z": "a0d6949c.443578",
        "name": "Run",
        "func": "var zeroMsg = { payload:0 };\nif(msg.payload == \"ON\"){\n    msg.payload = \"atvremote --address 192.168.0.107 --login_id **MY_LOGIN_ID** push_updates\";\nreturn [msg,zeroMsg];\n}\nif(msg.payload == \"OFF\"){\n    msg.kill = 'SIGTERM';\nreturn [msg,zeroMsg];\n}\n",
        "outputs": 2,
        "noerr": 0,
        "x": 250,
        "y": 300,
        "wires": [
            [
                "94004bee.36216"
            ],
            [
                "f8175196.93a868"
            ]
        ]
    },
    {
        "id": "46398b5c.9040e4",
        "type": "split",
        "z": "a0d6949c.443578",
        "name": "",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "x": 550,
        "y": 260,
        "wires": [
            [
                "906f3080.756b6"
            ]
        ]
    },
    {
        "id": "f775247b.bf3b98",
        "type": "inject",
        "z": "a0d6949c.443578",
        "name": "On",
        "topic": "",
        "payload": "ON",
        "payloadType": "str",
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "x": 90,
        "y": 320,
        "wires": [
            [
                "7bed54c9.660c04"
            ]
        ]
    },
    {
        "id": "5eb64c45.35bd84",
        "type": "inject",
        "z": "a0d6949c.443578",
        "name": "Off",
        "topic": "",
        "payload": "OFF",
        "payloadType": "str",
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "x": 90,
        "y": 360,
        "wires": [
            [
                "7bed54c9.660c04"
            ]
        ]
    },
    {
        "id": "906f3080.756b6",
        "type": "function",
        "z": "a0d6949c.443578",
        "name": "Play State",
        "func": "if(msg.payload == \"Play state: Paused\") {\n    msg.payload = 2;\nreturn msg;\n}\nif(msg.payload == \"Play state: Playing\") {\n    msg.payload = 1;\nreturn msg;\n}\nif(msg.payload == \"Play state: Fast backward\") {\n    msg.payload = 1;\nreturn msg;\n}\nif(msg.payload == \"Play state: Fast forward\") {\n    msg.payload = 1;\nreturn msg;\n}\nif(msg.payload == \"Play state: Idle\") {\n    msg.payload = 3;\nreturn msg;\n}\nif(msg.payload == \"Play state: No Media\") {\n    msg.payload = 3;\nreturn msg;\n}\nif(msg.payload == \"Play state: Loading\") {\n    msg.payload = 1;\nreturn msg;\n}\n\n/*var mediaState = global.get('mediaState')||0;\n//var bedMedia = global.get('bedMedia')||0;\n\nif(msg.payload == \"Play state: Paused\") {\n    mediaState = 2;\n    global.set('mediaState',mediaState);\n    msg.payload = mediaState;\nreturn msg;\n}\nif(msg.payload == \"Play state: Playing\") {\n    mediaState = 1;\n    global.set('mediaState',mediaState);\n    msg.payload = mediaState;\nreturn msg;\n}\nif(msg.payload == \"Play state: Fast backward\") {\n    mediaState = 1;\n    global.set('mediaState',mediaState);\n    msg.payload = mediaState;\nreturn msg;\n}\nif(msg.payload == \"Play state: Fast forward\") {\n    mediaState = 1;\n    global.set('mediaState',mediaState);\n    msg.payload = mediaState;\nreturn msg;\n}\nif(msg.payload == \"Play state: Idle\") {\n    mediaState = 3;\n    global.set('mediaState',mediaState);\n    msg.payload = mediaState;\nreturn msg;\n}\nif(msg.payload == \"Play state: No Media\") {\n    mediaState = 3;\n    global.set('mediaState',mediaState);\n    msg.payload = mediaState;\nreturn msg;\n}\nif(msg.payload == \"Play state: Loading\") {\n    mediaState = 1;\n    global.set('mediaState',mediaState);\n    msg.payload = mediaState;\nreturn msg;\n}\n*/\n/*\nif(msg.payload == \"Media type: Video\") {\n    bedMedia = 2;\n    global.set('bedMedia', bedMedia);\n}\nif(msg.payload == \"Media type: Music\") {\n    bedMedia = 3;\n    global.set('bedMedia', bedMedia);\n}\nif(msg.payload == \"Media type: TV\") {\n    bedMedia = 4;\n    global.set('bedMedia', bedMedia);\n}\nif(msg.payload == \"Media type: Unknown\") {\n    bedMedia = 1;\n    global.set('bedMedia', bedMedia);\n}\n*/",
        "outputs": 1,
        "noerr": 0,
        "x": 690,
        "y": 260,
        "wires": [
            [
                "f8175196.93a868"
            ]
        ]
    },
    {
        "id": "e013636.2a7e8a",
        "type": "rbe",
        "z": "a0d6949c.443578",
        "name": "Changed",
        "func": "rbe",
        "gap": "",
        "start": "",
        "inout": "out",
        "property": "payload",
        "x": 1020,
        "y": 300,
        "wires": [
            [
                "1f7bcf98.aa08b8",
                "d4765886.400f5"
            ]
        ]
    },
    {
        "id": "f296862b.2194e",
        "type": "openhab2-in",
        "z": "a0d6949c.443578",
        "name": "Onkyo",
        "controller": "21023ab5.9c90a6",
        "itemname": "Zone1",
        "x": 90,
        "y": 260,
        "wires": [
            [
                "7bed54c9.660c04"
            ],
            []
        ]
    },
    {
        "id": "1f7bcf98.aa08b8",
        "type": "openhab2-out",
        "z": "a0d6949c.443578",
        "name": "BigState",
        "controller": "21023ab5.9c90a6",
        "itemname": "MediaTVstate",
        "topic": "ItemCommand",
        "payload": "",
        "x": 1180,
        "y": 300,
        "wires": [
            []
        ]
    },
    {
        "id": "f8175196.93a868",
        "type": "trigger",
        "z": "a0d6949c.443578",
        "op1": "",
        "op2": "",
        "op1type": "nul",
        "op2type": "payl",
        "duration": "550",
        "extend": true,
        "units": "ms",
        "reset": "",
        "bytopic": "all",
        "name": "",
        "x": 860,
        "y": 300,
        "wires": [
            [
                "e013636.2a7e8a"
            ]
        ]
    },
    {
        "id": "d4765886.400f5",
        "type": "trigger",
        "z": "a0d6949c.443578",
        "op1": "",
        "op2": "OFF",
        "op1type": "nul",
        "op2type": "str",
        "duration": "14",
        "extend": true,
        "units": "min",
        "reset": "1",
        "bytopic": "all",
        "name": "",
        "x": 990,
        "y": 380,
        "wires": [
            [
                "6839a16f.0314f"
            ]
        ]
    },
    {
        "id": "6839a16f.0314f",
        "type": "exec",
        "z": "a0d6949c.443578",
        "command": "curl -k -H \"Content-Type: application/json\" -H \"AUTH: Z8o6f65kv8\" -X PUT -d '{\"KEYLIST\": [{\"CODESET\": 11,\"CODE\": 0,\"ACTION\":\"KEYPRESS\"}]}' https://192.168.0.176:7345/key_command/",
        "addpay": false,
        "append": "",
        "useSpawn": "false",
        "timer": "",
        "oldrc": false,
        "name": "CURL TV off",
        "x": 1170,
        "y": 380,
        "wires": [
            [],
            [],
            []
        ]
    },
    {
        "id": "21023ab5.9c90a6",
        "type": "openhab2-controller",
        "z": "",
        "name": "Openhab",
        "protocol": "http",
        "host": "localhost",
        "port": "8080",
        "path": "",
        "username": "",
        "password": ""
    }
]

(Just copy all of that and click “import” in node-red)