I did inject small time delays to let the activity update via Netflix. I am still testinf so those may need increased.
Example Sensor
[
{
"id": "nf_tab_simple",
"type": "tab",
"label": "Netflix Now Playing",
"disabled": false,
"info": "",
"env": []
},
{
"id": "nf_inject_manual",
"type": "inject",
"z": "nf_tab_simple",
"name": "Manual refresh",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "manual",
"payloadType": "str",
"x": 150,
"y": 80,
"wires": [
[
"nf_cachebust"
]
]
},
{
"id": "nf_events",
"type": "server-state-changed",
"z": "nf_tab_simple",
"name": "Android TV Shield",
"server": "nf_ha_server",
"version": 6,
"outputs": 1,
"exposeAsEntityConfig": "",
"entities": {
"entity": [
"media_player.android_tv_shield"
],
"substring": [],
"regex": []
},
"outputInitially": true,
"stateType": "str",
"ifState": "",
"ifStateType": "str",
"ifStateOperator": "is",
"outputOnlyOnStateChange": false,
"for": "0",
"forType": "num",
"forUnits": "minutes",
"ignorePrevStateNull": true,
"ignorePrevStateUnknown": true,
"ignorePrevStateUnavailable": true,
"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": 220,
"wires": [
[
"nf_route"
]
]
},
{
"id": "nf_route",
"type": "function",
"z": "nf_tab_simple",
"name": "Playing? / Pause?",
"func": "const ns = msg.data && msg.data.new_state;\nconst os = msg.data && msg.data.old_state;\nif (!ns) return [null, null];\n\nfunction isNetflixPlaying(st) {\n if (!st || st.state !== 'playing') return false;\n const app = (st.attributes && st.attributes.app_name) || '';\n const id = (st.attributes && st.attributes.app_id) || '';\n return app === 'Netflix' || id === 'com.netflix.ninja';\n}\n\nconst nowNetflix = isNetflixPlaying(ns);\nconst wasNetflix =\n flow.get('wasNetflixPlaying') === true ||\n isNetflixPlaying(os);\n\n// Idle / not Netflix: reset Wait 5s so Re-read + Still Netflix never run\nif (!nowNetflix) {\n flow.set('awaitingPlayConfirm', false);\n flow.set('wasNetflixPlaying', false);\n flow.set('scrapeGen', (flow.get('scrapeGen') || 0) + 1);\n const cancel = { reset: true, topic: msg.topic || '' };\n if (wasNetflix || !os) {\n node.status({\n fill: 'grey',\n shape: 'ring',\n text: wasNetflix ? 'Idle - reset wait + No Title' : 'Startup - reset + No Title'\n });\n return [cancel, { payload: 'No Title' }];\n }\n node.status({\n fill: 'grey',\n shape: 'ring',\n text: 'Idle - reset wait (' + (ns.state || '?') + ')'\n });\n return [cancel, null];\n}\n\n// Netflix playing â arm wait; Re-read only after 5s if still armed\nflow.set('awaitingPlayConfirm', true);\nnode.status({ fill: 'blue', shape: 'dot', text: 'Netflix playing - wait 5s' });\nreturn [msg, null];",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 400,
"y": 220,
"wires": [
[
"nf_debounce"
],
[
"nf_sensor_now"
]
]
},
{
"id": "nf_debounce",
"type": "trigger",
"z": "nf_tab_simple",
"name": "Wait 5s (play only)",
"op1": "",
"op2": "",
"op1type": "nul",
"op2type": "pay",
"duration": "5",
"extend": true,
"overrideDelay": false,
"units": "s",
"reset": "",
"bytopic": "all",
"topic": "topic",
"outputs": 1,
"x": 650,
"y": 160,
"wires": [
[
"nf_wait_armed"
]
]
},
{
"id": "nf_wait_armed",
"type": "function",
"z": "nf_tab_simple",
"name": "Wait still armed?",
"func": "if (flow.get('awaitingPlayConfirm') !== true) {\n node.status({ fill: 'grey', shape: 'ring', text: 'Stopped (idle/reset)' });\n return null;\n}\nnode.status({ fill: 'blue', shape: 'dot', text: 'Armed - re-read' });\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 880,
"y": 100,
"wires": [
[
"nf_current"
]
]
},
{
"id": "nf_current",
"type": "api-current-state",
"z": "nf_tab_simple",
"name": "Re-read Shield",
"server": "nf_ha_server",
"version": 3,
"outputs": 1,
"halt_if": "",
"halt_if_type": "str",
"halt_if_compare": "is",
"entity_id": "media_player.android_tv_shield",
"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_type": "str",
"x": 1100,
"y": 160,
"wires": [
[
"nf_gate"
]
]
},
{
"id": "nf_gate",
"type": "function",
"z": "nf_tab_simple",
"name": "Still Netflix playing?",
"func": "// Idle/reset already cleared awaitingPlayConfirm â stop here\nif (flow.get('awaitingPlayConfirm') !== true) {\n node.status({ fill: 'grey', shape: 'ring', text: 'Stopped (idle/reset)' });\n return [null, null];\n}\n\nconst entity = msg.data;\nif (!entity || entity.state === undefined) {\n flow.set('awaitingPlayConfirm', false);\n node.status({ fill: 'red', shape: 'ring', text: 'No entity data' });\n return [null, null];\n}\n\nconst app = (entity.attributes && entity.attributes.app_name) || '';\nconst id = (entity.attributes && entity.attributes.app_id) || '';\nconst playing = entity.state === 'playing' && (app === 'Netflix' || id === 'com.netflix.ninja');\nconst wasPlaying = flow.get('wasNetflixPlaying') === true;\n\n// Wait finished but no longer Netflix â stop scrape path (route already cleared title)\nif (!playing) {\n flow.set('awaitingPlayConfirm', false);\n flow.set('wasNetflixPlaying', false);\n flow.set('scrapeGen', (flow.get('scrapeGen') || 0) + 1);\n node.status({ fill: 'grey', shape: 'ring', text: 'Stopped (not Netflix)' });\n return [null, null];\n}\n\nflow.set('awaitingPlayConfirm', false);\n\nif (wasPlaying) {\n node.status({ fill: 'blue', shape: 'ring', text: 'Already playing' });\n return [null, null];\n}\n\nflow.set('wasNetflixPlaying', true);\nconst gen = (flow.get('scrapeGen') || 0) + 1;\nflow.set('scrapeGen', gen);\nmsg.scrapeGen = gen;\nnode.status({ fill: 'green', shape: 'dot', text: 'Netflix playing - scrape' });\nreturn [msg, null];",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1340,
"y": 160,
"wires": [
[
"nf_cachebust"
],
[
"nf_sensor_now"
]
]
},
{
"id": "nf_cachebust",
"type": "function",
"z": "nf_tab_simple",
"name": "Cache bust URL",
"func": "msg.url = 'https://www.netflix.com/viewingactivity?_=' + Date.now();\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 880,
"y": 80,
"wires": [
[
"nf_http"
]
]
},
{
"id": "nf_http",
"type": "http request",
"z": "nf_tab_simple",
"name": "Netflix Viewing Activity",
"method": "GET",
"ret": "txt",
"paytoqs": "ignore",
"url": "",
"tls": "",
"persist": false,
"proxy": "",
"insecureHTTPParser": false,
"authType": "",
"senderr": false,
"headers": [
{
"keyType": "other",
"keyValue": "cookie",
"valueType": "other",
"valueValue": "your-netflix-cookie"
},
{
"keyType": "other",
"keyValue": "User-Agent",
"valueType": "other",
"valueValue": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
},
{
"keyType": "other",
"keyValue": "Cache-Control",
"valueType": "other",
"valueValue": "no-cache"
},
{
"keyType": "other",
"keyValue": "Accept-Language",
"valueType": "other",
"valueValue": "en-US,en;q=0.9"
}
],
"x": 1120,
"y": 80,
"wires": [
[
"nf_parse"
]
]
},
{
"id": "nf_parse",
"type": "function",
"z": "nf_tab_simple",
"name": "Parse viewedItems",
"func": "const gen = msg.scrapeGen;\nif (gen != null && gen !== flow.get('scrapeGen')) {\n node.status({ fill: 'grey', shape: 'ring', text: 'Stale scrape ignored' });\n return null;\n}\nif (flow.get('wasNetflixPlaying') !== true) {\n node.status({ fill: 'grey', shape: 'ring', text: 'Left Netflix - ignore scrape' });\n return null;\n}\n\nconst html = typeof msg.payload === 'string' ? msg.payload : String(msg.payload || '');\n\nif (!html || html.length < 100) {\n msg.payload = 'No Title';\n node.status({ fill: 'red', shape: 'ring', text: 'Empty response' });\n return msg;\n}\n\nif (/\\/login/i.test(msg.responseUrl || '') || /name=[\"']login/i.test(html)) {\n msg.payload = 'Cookie expired';\n node.status({ fill: 'red', shape: 'ring', text: 'Cookie expired' });\n return msg;\n}\n\nfunction extractViewedItems(page) {\n const marker = '\"viewedItems\":';\n const start = page.indexOf(marker);\n if (start < 0) return null;\n let i = start + marker.length;\n while (page[i] && /\\s/.test(page[i])) i++;\n if (page[i] !== '[') return null;\n\n let depth = 0;\n let end = i;\n for (; end < page.length; end++) {\n const c = page[end];\n if (c === '[') depth++;\n else if (c === ']') {\n depth--;\n if (depth === 0) {\n end++;\n break;\n }\n }\n }\n\n const raw = page.slice(i, end);\n const jsonText = raw.replace(/\\\\x([0-9A-Fa-f]{2})/g, (_, h) =>\n String.fromCharCode(parseInt(h, 16))\n );\n return JSON.parse(jsonText);\n}\n\nfunction formatTitle(item) {\n if (item.seriesTitle && item.episodeTitle) {\n const season = item.seasonDescriptor ? item.seasonDescriptor + ': ' : '';\n return (item.seriesTitle + ': ' + season + item.episodeTitle).replace(/\\s+/g, ' ').trim();\n }\n if (item.seriesTitle && item.title) {\n return (item.seriesTitle + ': ' + item.title).replace(/\\s+/g, ' ').trim();\n }\n return item.title || item.episodeTitle || 'No Title';\n}\n\nlet items;\ntry {\n items = extractViewedItems(html);\n} catch (err) {\n node.error(err);\n msg.payload = 'No Title';\n node.status({ fill: 'red', shape: 'ring', text: 'Parse error' });\n return msg;\n}\n\nif (!items || !items.length) {\n msg.payload = 'No Title';\n node.status({ fill: 'yellow', shape: 'ring', text: 'No viewedItems' });\n return msg;\n}\n\nmsg.payload = formatTitle(items[0]);\nnode.status({ fill: 'green', shape: 'dot', text: msg.payload });\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1370,
"y": 80,
"wires": [
[
"nf_sensor_now",
"nf_debug"
]
]
},
{
"id": "nf_sensor_now",
"type": "ha-sensor",
"z": "nf_tab_simple",
"name": "Netflix Now Playing",
"entityConfig": "nf_entity_now",
"version": 0,
"state": "payload",
"stateType": "msg",
"attributes": [],
"inputOverride": "allow",
"outputProperties": [],
"x": 1630,
"y": 220,
"wires": [
[]
]
},
{
"id": "nf_debug",
"type": "debug",
"z": "nf_tab_simple",
"name": "title",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 1590,
"y": 40,
"wires": []
},
{
"id": "nf_entity_now",
"type": "ha-entity-config",
"server": "nf_ha_server",
"deviceConfig": "",
"name": "Netflix Now Playing",
"version": 6,
"entityType": "sensor",
"haConfig": [
{
"property": "name",
"value": "Netflix Now Playing"
},
{
"property": "icon",
"value": "mdi:netflix"
},
{
"property": "entity_picture",
"value": ""
},
{
"property": "entity_category",
"value": ""
},
{
"property": "device_class",
"value": ""
},
{
"property": "unit_of_measurement",
"value": ""
},
{
"property": "state_class",
"value": ""
}
],
"resend": true,
"debugEnabled": false
},
{
"id": "nf_ha_server",
"type": "server",
"name": "Home Assistant",
"version": 6,
"addon": true,
"rejectUnauthorizedCerts": true,
"ha_boolean": [
"y",
"yes",
"true",
"on",
"home",
"open"
],
"connectionDelay": true,
"cacheJson": true,
"heartbeat": false,
"heartbeatInterval": 30,
"areaSelector": "friendlyName",
"deviceSelector": "friendlyName",
"entitySelector": "friendlyName",
"statusSeparator": "at: ",
"statusYear": "hidden",
"statusMonth": "short",
"statusDay": "numeric",
"statusHourCycle": "h23",
"statusTimeFormat": "h:m",
"enableGlobalContextStore": true
}
]
Date format from your previous posted example needed updated based on how Netflix reads it. US is MM-DD-YY , EU is DD-MM-YY from my understanding.
This method works the best by disabling the AutoPlay settings here
schumi2004:
The template might ineed be dropped but for me it was necessary since i would other wise lose controls (play,pause,vol, back next track etc) when using media player card and different streaming services/apps
I just mentioned this because HA templates have come a long way. Letâs tackle that after we secure a solid solution to media info (like Titles ) for services that do not report natively to HA.
Edit: I built two local integrations :
Android TV Remote Local - combines the best of Android Debug Bridge and Android TV Remote
Media player template Local - adds the ability to template the icon (entity_picture ), use an entity state in the value_template: and keep media_image_url_template: as the background attribute.
The Node-Red Sensor was updated to accommodate these changes.