This post is going to cover normalizing all media_player volume levels with
Home Assistant and Node-RED.
If you haven’t already, I’d recommend reading my previous blog post on
sending Text-to-Speech notifications which
dives a bit deeper on the optional accessibility Subflow this Flow consumes.
This is one of my favorite automations in my house and I think you will enjoy it too! I wrote this automation for several reasons:
This addon is amazing as it normalizes all of your Alexa enabled devices such
as the
Amazon Echo
or ecobee Switch+
into a media_player that can be used by Home Assistant.
This Automation Flow controls the volume levels by calling the
media_player.volume_set service. I’d recommend testing it first by opening
the Home Assistant Developer tools and navigating to the Services tab.
Next, start playing some music on your media_player and call the
media_player.volume_set service with a payload of:
entity_id: media_player.amazon_echo_plus
volume_level: 0.6
If you don’t hear a volume change, try a different volume_level in case the
the device is already at that level. If it’s not then stop and check the logs.
This will help diagnose why the device isn’t working and save you lots of time
before continuing.
I highly recommend adding multiple
Input Booleans
that controls if a specific automation or all automations can run. It’s always
nice to be able to turn on or off a specific automation or all in the case of
maintenance. Here are the three input_booleans that are used in this
Automation Flow.
If you choose a different name, be sure to update the code below.
automation_enable:
name: Enable Automations
icon: mdi:home-automation
automation_normalize_volume:
name: Automation - Automatically Normalize Volume throughout the day
icon: mdi:volume-high
automation_notifications:
name: Notify when an automation is triggered
icon: mdi:home-automation
This Subflow will get the current devices volume level, compare the current and desired state. If changes are required, it will call the required service to change the devices volume level.
graph TD InputNode(Input) --> ACurrentStateNode(fas:fa-database Get Current Volume Level) ACurrentStateNode --> BFunctionNode(fas:fa-code Set Volume Level Payload) BFunctionNode --> CCallServiceNode(fas:fa-play Set Volume Level) BFunctionNode --> DCallSubflowNode(fas:fa-sliders-h Send Automation Speech Notification) CCallServiceNode --> OutputNode(Output 1) StatusNode(fas:fa-heartbeat Status: All) --> StatusOutputNode(Status) style StatusNode fill:#ECF5FF style StatusOutputNode fill:#FAFAFB style InputNode fill:#FAFAFB style ACurrentStateNode fill:#66ACFD style BFunctionNode fill:#FBB68F style CCallServiceNode fill:#66ACFD style DCallSubflowNode fill:#FF75A1 style OutputNode fill:#FAFAFB linkStyle default stroke-width:2px,fill:none,stroke:#CCD0D4
Let’s break down the Subflow. The incoming message gets passed to the
Get Current Volume Level Current State node to populate msg.data with the
entity. This is then passed to the Set Volume Level Payload node which checks
to see if any changes are required to meet the desired volume level and outputs
Text-to-Speech (TTS) status messages. If changes are required, the
Set Volume Level node will call the media_player.volume_set service.
Here is the JavaScript code contained in the Set Volume Level Payload
Function node to smartly adjust the volume only if it is needed.
const entity = msg.payload && msg.payload.entity_id;
const attributes = msg.data && msg.data.attributes
if (!entity || !attributes) {
node.status({ fill: "red", shape: "dot", text: "Invalid volume payload" });
return [null, null];
}
const desired_volume_level = msg.payload.volume_level || 0;
if (desired_volume_level < 0.0 || desired_volume_level > 1.0) {
let message = "Volume level must be between 0 and 1.";
node.status({ fill: "red", shape: "dot", text: message });
node.error(message);
return [null, {
payload: {
entity_id: entity,
message: message
}
}];
}
if (desired_volume_level === attributes.volume_level) {
let message = "Volume level is already at the desired level.";
node.status({ fill: "grey", shape: "dot", text: message });
node.log(message);
return [null, {
payload: {
entity_id: entity,
message: message
}
}];
}
const volumePayload = {
payload: {
data: {
entity_id: entity,
volume_level: desired_volume_level
}
}
};
let message = "Setting Volume Level to " + (desired_volume_level * 100) + " percent.";
const speechPayload = {
payload: {
entity_id: entity,
message: message
}
};
node.status({ fill: "green", shape: "dot", text: message });
return [volumePayload, speechPayload];
As you an see it will output a friendly speech notification payload that will
be sent to the Send Automation Speech Notification Subflow. If you are not
using the alexa_media_player Home Assistant addon, you may want to update the
you’ll want to update Set Volume Level Payload Function node to take in a
volume range you are expecting.
You can trigger this Subflow by passing a message object with the following payload. I recommend using a Inject Node to test this out.
{
"entity_id": "media_player.amazon_echo_plus",
"volume_level":0.4
}
This subflow will get all media_player' and then send them one by one to the
Set Volume Level Subflow.
graph TD InputNode(Input) --> AFunctionNode(fas:fa-code Parse Volume Level) AFunctionNode --> BGetEntitiesNode(fas:fa-search Get All Media Players) BGetEntitiesNode --> CChangeNode(fas:fa-random Set Volume Level Payload) CChangeNode --> DSubflowNode(fas:fa-sliders-h Set Volume Level) DSubflowNode --> OutputNode(Output) StatusNode(fas:fa-heartbeat Status: All) --> StatusOutputNode(Status) style StatusNode fill:#ECF5FF style StatusOutputNode fill:#FAFAFB style InputNode fill:#FAFAFB style AFunctionNode fill:#FBB68F style BGetEntitiesNode fill:#66ACFD style CChangeNode fill:#FEA530 style DSubflowNode fill:#FF75A1 style OutputNode fill:#FAFAFB linkStyle default stroke-width:2px,fill:none,stroke:#CCD0D4
You can trigger this Subflow by passing a message object with the
following float (e.g. 0.1) payload. I recommend using a Inject Node
to test this out.
This flow brings the two previous sub flows together to normalize all volume levels based on the time of the day. In my home, I set the level to 30% from 9:00am to 9:00pm, otherwise I set to 10%.
To have the flow set the volume levels at different times of the day, I use the
BigTimer Node
with an On Time of 09:00 and a Off Time of 21:00. Then I set the
ON Msg to .3 and a OFF Msg to .1. When the timer turns on and turns off,
it will check to see if my automation flags are turned on. If they are, it will
enumerate over all media devices and set the volume based on the timers output
message (e.g., .1 or .3).
graph TD ABigTimerNode(fas:fa-clock Normalize Volume) --> BCurrentStateNode(fas:fa-database Automations Enabled?) BCurrentStateNode --> CCurrentStateNode(fas:fa-database Normalize Volume Levels?) CCurrentStateNode --> DRbeNode(fas:fa-scroll Only allow changed values) DRbeNode --> ECallSubflow(fas:fa-sliders-h Set Volume Level on All Media Devices Subflow) StatusNode(fas:fa-heartbeat Status: All) --> StatusOutputNode(Status) style StatusNode fill:#ECF5FF style StatusOutputNode fill:#FAFAFB style ABigTimerNode fill:#3DB39F style BCurrentStateNode fill:#66ACFD style CCurrentStateNode fill:#66ACFD style DRbeNode fill:#FEB95E style ECallSubflow fill:#FF75A1 linkStyle default stroke-width:2px,fill:none,stroke:#CCD0D4
You can import this Flow and all Subflows shown above by importing the following JSON.
[
{
"id": "7a62a2d3.c0f4a4",
"type": "Subflow",
"name": "Send Automation Speech Notification",
"info": "",
"category": "",
"in": [
{
"x": 60,
"y": 100,
"wires": [
{
"id": "87411254.a6ed18"
}
]
}
],
"out": [
{
"x": 1120,
"y": 100,
"wires": [
{
"id": "d44cc51a.be0668",
"port": 0
}
]
}
],
"status": {
"x": 220,
"y": 40,
"wires": [
{
"id": "aef53056.742438",
"port": 0
}
]
}
},
{
"id": "89300b1.595bcf8",
"type": "function",
"z": "7a62a2d3.c0f4a4",
"name": "Set Speech Payload",
"func": "const entity = flow.get(\"$parent.speech_entity_id\") || (msg.payload && msg.payload.entity_id) || \"media_player.office_echo_plus\";\nconst message = (msg.payload && msg.payload.message) || \"Automation provided no message\";\nconst announcement = msg.payload && msg.payload.announcement;\n\nif (announcement) {\n node.status({ fill: \"green\", shape: \"dot\", text: \"Announce message:\" + message });\n return {\n payload:{\n data: {\n message: message,\n data: { \"type\": \"announce\", \"method\": \"all\" },\n target: !!entity ? [entity] : []\n }\n }\n };\n}\n\nnode.status({ fill: \"green\", shape: \"dot\", text: \"TTS message:\" + message });\nreturn {\n payload:{\n data: {\n message: message,\n data: { type: \"tts\" },\n target: [entity]\n }\n }\n};",
"outputs": 1,
"noerr": 0,
"x": 700,
"y": 100,
"wires": [
[
"d44cc51a.be0668"
]
]
},
{
"id": "d44cc51a.be0668",
"type": "api-call-service",
"z": "7a62a2d3.c0f4a4",
"name": "Send Speech Notification",
"server": "61956bd4.93df44",
"version": 1,
"debugenabled": false,
"service_domain": "notify",
"service": "alexa_media",
"entityId": "",
"data": "",
"dataType": "json",
"mergecontext": "",
"output_location": "payload",
"output_location_type": "msg",
"mustacheAltTags": false,
"x": 950,
"y": 100,
"wires": [
[]
]
},
{
"id": "6add9bc6.4c3624",
"type": "api-current-state",
"z": "7a62a2d3.c0f4a4",
"name": "Speech Notifications?",
"server": "61956bd4.93df44",
"version": 1,
"outputs": 2,
"halt_if": "true",
"halt_if_type": "bool",
"halt_if_compare": "is",
"override_topic": false,
"entity_id": "input_boolean.automation_notifications",
"state_type": "habool",
"state_location": "",
"override_payload": "none",
"entity_location": "",
"override_data": "none",
"blockInputOverrides": false,
"x": 440,
"y": 140,
"wires": [
[
"89300b1.595bcf8"
],
[]
],
"outputLabels": [
"",
"enabled"
]
},
{
"id": "aef53056.742438",
"type": "status",
"z": "7a62a2d3.c0f4a4",
"name": "",
"scope": null,
"x": 100,
"y": 40,
"wires": [
[]
]
},
{
"id": "87411254.a6ed18",
"type": "function",
"z": "7a62a2d3.c0f4a4",
"name": "Check for overrides",
"func": "const alwaysSpeak = msg.payload && msg.payload.announcement\nif (alwaysSpeak) {\n return [msg, null];\n} else {\n return [null, msg];\n}",
"outputs": 2,
"noerr": 0,
"x": 210,
"y": 100,
"wires": [
[
"89300b1.595bcf8"
],
[
"6add9bc6.4c3624"
]
],
"outputLabels": [
"Bypass notification",
"Check for notifications"
]
},
{
"id": "974adda.d2ea92",
"type": "Subflow",
"name": "Set Volume Level",
"info": "Set Volume Level",
"category": "",
"in": [
{
"x": 80,
"y": 120,
"wires": [
{
"id": "4c86fd47.0875e4"
}
]
}
],
"out": [
{
"x": 960,
"y": 100,
"wires": [
{
"id": "263d05a1.5b6672",
"port": 0
}
]
}
],
"status": {
"x": 240,
"y": 40,
"wires": [
{
"id": "7a662cd5.162a24",
"port": 0
}
]
}
},
{
"id": "563c252e.ce31e4",
"type": "function",
"z": "974adda.d2ea92",
"name": "Set Volume Level Payload",
"func": "const entity = msg.payload && msg.payload.entity_id;\nconst attributes = msg.data && msg.data.attributes\nif (!entity || !attributes) {\n node.status({ fill: \"red\", shape: \"dot\", text: \"Invalid volume payload\" });\n return [null, null];\n}\n\nconst desired_volume_level = msg.payload.volume_level || 0;\nif (desired_volume_level < 0.0 || desired_volume_level > 1.0) {\n let message = \"Volume level must be between 0 and 1.\"; \n node.status({ fill: \"red\", shape: \"dot\", text: message });\n node.error(message);\n \n return [null, { \n payload: {\n entity_id: entity,\n message: message\n }\n }];\n}\n\nif (desired_volume_level === attributes.volume_level) {\n let message = \"Volume level is already at the desired level.\"; \n node.status({ fill: \"grey\", shape: \"dot\", text: message });\n node.log(message);\n return [null, { \n payload: {\n entity_id: entity,\n message: message\n }\n }];\n}\n\nconst volumePayload = { \n payload: {\n data: {\n entity_id: entity,\n volume_level: desired_volume_level\n } \n }\n};\n\nlet message = \"Setting Volume Level to \" + (desired_volume_level * 100) + \" percent.\";\nconst speechPayload = { \n payload: {\n entity_id: entity,\n message: message\n }\n};\n\nnode.status({ fill: \"green\", shape: \"dot\", text: message });\nreturn [volumePayload, speechPayload];",
"outputs": 2,
"noerr": 0,
"x": 510,
"y": 120,
"wires": [
[
"263d05a1.5b6672"
],
[
"b41308c0.f41e9"
]
],
"inputLabels": [
"Volume Percentage"
],
"outputLabels": [
"Volume Level Payload",
"Speech Notification Payload"
]
},
{
"id": "263d05a1.5b6672",
"type": "api-call-service",
"z": "974adda.d2ea92",
"name": "Set Volume Level",
"server": "61956bd4.93df44",
"version": 1,
"debugenabled": false,
"service_domain": "media_player",
"service": "volume_set",
"entityId": "",
"data": "",
"dataType": "json",
"mergecontext": "",
"output_location": "payload",
"output_location_type": "msg",
"mustacheAltTags": false,
"x": 770,
"y": 100,
"wires": [
[]
]
},
{
"id": "b41308c0.f41e9",
"type": "Subflow:7a62a2d3.c0f4a4",
"z": "974adda.d2ea92",
"name": "",
"env": [],
"x": 830,
"y": 160,
"wires": [
[]
]
},
{
"id": "4c86fd47.0875e4",
"type": "api-current-state",
"z": "974adda.d2ea92",
"name": "Get Current Volume Level",
"server": "61956bd4.93df44",
"version": 1,
"outputs": 1,
"halt_if": "",
"halt_if_type": "str",
"halt_if_compare": "is",
"override_topic": true,
"entity_id": "",
"state_type": "str",
"state_location": "",
"override_payload": "none",
"entity_location": "data",
"override_data": "msg",
"blockInputOverrides": false,
"x": 250,
"y": 120,
"wires": [
[
"563c252e.ce31e4"
]
]
},
{
"id": "7a662cd5.162a24",
"type": "status",
"z": "974adda.d2ea92",
"name": "",
"scope": null,
"x": 120,
"y": 40,
"wires": [
[]
]
},
{
"id": "76be143e.dbb0e4",
"type": "Subflow",
"name": "Set Volume Level on All Media Devices",
"info": "",
"category": "",
"in": [
{
"x": 100,
"y": 120,
"wires": [
{
"id": "642c5f33.bd92a8"
}
]
}
],
"out": [
{
"x": 1120,
"y": 120,
"wires": [
{
"id": "15d5ddae.8f64c2",
"port": 0
}
]
}
],
"env": [],
"color": "#DDAA99",
"status": {
"x": 1120,
"y": 40,
"wires": [
{
"id": "ba88f8d5.b5d648",
"port": 0
},
{
"id": "15d5ddae.8f64c2",
"port": 0
}
]
}
},
{
"id": "f467a711.24ca3",
"type": "ha-get-entities",
"z": "76be143e.dbb0e4",
"server": "61956bd4.93df44",
"name": "Get All Media Players",
"rules": [
{
"property": "attributes.supported_features",
"logic": "is",
"value": "56253",
"valueType": "num"
},
{
"property": "attributes.available",
"logic": "is",
"value": "true",
"valueType": "bool"
}
],
"output_type": "split",
"output_empty_results": true,
"output_location_type": "msg",
"output_location": "payload",
"output_results_count": 1,
"x": 480,
"y": 120,
"wires": [
[
"1fe46c89.0e0543"
]
]
},
{
"id": "1fe46c89.0e0543",
"type": "change",
"z": "76be143e.dbb0e4",
"name": "Set Volume Level Payload",
"rules": [
{
"t": "set",
"p": "payload.volume_level",
"pt": "msg",
"to": "volume_level",
"tot": "flow"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 730,
"y": 120,
"wires": [
[
"15d5ddae.8f64c2"
]
]
},
{
"id": "642c5f33.bd92a8",
"type": "function",
"z": "76be143e.dbb0e4",
"name": "Parse Volume Level",
"func": "const volumeLevel = parseFloat(msg.payload);\nnode.status({ fill: \"green\", shape: \"dot\", text: \"Volume Level: \" + volumeLevel });\nflow.set(\"volume_level\", volumeLevel);\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 250,
"y": 120,
"wires": [
[
"f467a711.24ca3"
]
]
},
{
"id": "15d5ddae.8f64c2",
"type": "Subflow:974adda.d2ea92",
"z": "76be143e.dbb0e4",
"name": "",
"env": [],
"x": 970,
"y": 120,
"wires": [
[]
]
},
{
"id": "ba88f8d5.b5d648",
"type": "status",
"z": "76be143e.dbb0e4",
"name": "",
"scope": null,
"x": 140,
"y": 40,
"wires": [
[]
]
},
{
"id": "3c07a459.ce903c",
"type": "tab",
"label": "Normalize Volume",
"disabled": false,
"info": "Normalize All Volume"
},
{
"id": "e949c86f.c9ca9",
"type": "api-current-state",
"z": "3c07a459.ce903c",
"name": "Automations Enabled?",
"server": "61956bd4.93df44",
"version": 1,
"outputs": 2,
"halt_if": "true",
"halt_if_type": "bool",
"halt_if_compare": "is",
"override_topic": false,
"entity_id": "input_boolean.automation_enable",
"state_type": "habool",
"state_location": "",
"override_payload": "none",
"entity_location": "",
"override_data": "none",
"blockInputOverrides": false,
"x": 560,
"y": 100,
"wires": [
[
"ec5308f5.aefff"
],
[]
],
"outputLabels": [
"",
"enabled"
]
},
{
"id": "ec5308f5.aefff",
"type": "api-current-state",
"z": "3c07a459.ce903c",
"name": "Normalize Volume Levels?",
"server": "61956bd4.93df44",
"version": 1,
"outputs": 2,
"halt_if": "true",
"halt_if_type": "bool",
"halt_if_compare": "is",
"override_topic": false,
"entity_id": "input_boolean.automation_normalize_volume",
"state_type": "habool",
"state_location": "",
"override_payload": "none",
"entity_location": "",
"override_data": "none",
"blockInputOverrides": false,
"x": 810,
"y": 100,
"wires": [
[
"bde48ad8.814ac"
],
[]
],
"outputLabels": [
"",
"enabled"
]
},
{
"id": "dbf47096.3a7dc",
"type": "inject",
"z": "3c07a459.ce903c",
"name": "",
"topic": "",
"payload": "on",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"x": 110,
"y": 80,
"wires": [
[
"4608374e.2fd4d8"
]
]
},
{
"id": "6eace9df.a5aa68",
"type": "inject",
"z": "3c07a459.ce903c",
"name": "",
"topic": "",
"payload": "off",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"x": 110,
"y": 120,
"wires": [
[
"4608374e.2fd4d8"
]
]
},
{
"id": "4608374e.2fd4d8",
"type": "bigtimer",
"z": "3c07a459.ce903c",
"outtopic": "",
"outpayload1": ".3",
"outpayload2": ".1",
"name": "Normalize Volume",
"comment": "Set volume to 30% from 9:00am to 9:00pm, otherwise set to 10%",
"lat": "44.50",
"lon": "-88.06",
"starttime": "540",
"endtime": "1260",
"startoff": "0",
"endoff": 0,
"startoff2": "",
"endoff2": "",
"offs": 0,
"outtext1": "",
"outtext2": "",
"timeout": "240",
"sun": true,
"mon": true,
"tue": true,
"wed": true,
"thu": true,
"fri": true,
"sat": true,
"jan": true,
"feb": true,
"mar": true,
"apr": true,
"may": true,
"jun": true,
"jul": true,
"aug": true,
"sep": true,
"oct": true,
"nov": true,
"dec": true,
"day1": 0,
"month1": 0,
"day2": 0,
"month2": 0,
"day3": 0,
"month3": 0,
"day4": 0,
"month4": 0,
"day5": 0,
"month5": 0,
"day6": 0,
"month6": 0,
"day7": "",
"month7": "",
"day8": "",
"month8": "",
"day9": "",
"month9": "",
"day10": "",
"month10": "",
"day11": "",
"month11": "",
"day12": "",
"month12": "",
"d1": 0,
"w1": 0,
"d2": 0,
"w2": 0,
"d3": 0,
"w3": 0,
"d4": 0,
"w4": 0,
"d5": 0,
"w5": 0,
"d6": 0,
"w6": 0,
"xday1": "0",
"xmonth1": "0",
"xday2": "0",
"xmonth2": "0",
"xday3": 0,
"xmonth3": 0,
"xday4": 0,
"xmonth4": 0,
"xday5": 0,
"xmonth5": 0,
"xday6": 0,
"xmonth6": 0,
"xd1": 0,
"xw1": 0,
"xd2": 0,
"xw2": 0,
"xd3": 0,
"xw3": 0,
"xd4": 0,
"xw4": 0,
"xd5": 0,
"xw5": 0,
"xd6": 0,
"xw6": 0,
"suspend": false,
"random": false,
"repeat": false,
"atstart": false,
"odd": false,
"even": false,
"x": 340,
"y": 100,
"wires": [
[
"e949c86f.c9ca9"
],
[],
[]
]
},
{
"id": "418865ec.975eec",
"type": "inject",
"z": "3c07a459.ce903c",
"name": "",
"topic": "",
"payload": "auto",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"x": 110,
"y": 160,
"wires": [
[
"4608374e.2fd4d8"
]
]
},
{
"id": "e314dd72.39afe",
"type": "inject",
"z": "3c07a459.ce903c",
"name": "",
"topic": "",
"payload": "manual",
"payloadType": "str",
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"x": 110,
"y": 200,
"wires": [
[
"4608374e.2fd4d8"
]
]
},
{
"id": "bde48ad8.814ac",
"type": "rbe",
"z": "3c07a459.ce903c",
"name": "Only allow changed values",
"func": "rbe",
"gap": "",
"start": "",
"inout": "out",
"property": "payload",
"x": 480,
"y": 180,
"wires": [
[
"4173c48c.381f84"
]
]
},
{
"id": "4173c48c.381f84",
"type": "Subflow:76be143e.dbb0e4",
"z": "3c07a459.ce903c",
"name": "",
"env": [],
"x": 790,
"y": 180,
"wires": [
[]
]
},
{
"id": "d45759b3.b76fe8",
"type": "comment",
"z": "3c07a459.ce903c",
"name": "Set volume to 30% from 9:00am to 9:00pm, otherwise set to 10%",
"info": "",
"x": 270,
"y": 40,
"wires": []
},
{
"id": "61956bd4.93df44",
"type": "server",
"z": "",
"name": "Home Assistant",
"legacy": false,
"addon": true,
"rejectUnauthorizedCerts": true,
"ha_boolean": "y|yes|true|on|home|open",
"connectionDelay": true
}
]
I personally prefer writing all my automations in NodeRed as it helps me visually see the flow of the automation. I’d love to hear what you like and if you’d like to see automations written using the built in Home Assistant Automations.