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 automation normalizes all volume levels in my home based on the time of day using the Amazon Alexa Media Player Integration to bring in all my Amazon Echo and ecobee Switch+ devices into Home Assistant.
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:
- I wanted all speakers to be quiet at bed time. This prevents someone from getting blasted at 2:00 am in the morning.
- In the morning after I’ve had my cup of coffee, I want to increase the volume to an acceptable level.
- Automation allows for reuse via sub-flows. This means you can trigger it for other use cases like friends are over and music is playing, increase or decrease the volume.
- One less thing I have to think about, or constantly touch to change. In todays pandemic, this is also one less thing you potentially have to clean!
- You can use this same routine for increasing the volume when triggering an alarm.
- All speakers are at the same volume level when playing music on every speaker.
This Automation Flow is comprised of two different Subflows.
Alexa Media Player Custom Component
If you haven’t done so already, install the Alexa Media Player Custom Component if you are using Alexa, otherwise skip this section.
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.
Service to set volume level.
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.
Input Boolean to control automations
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_boolean
s 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
Set Volume Level Subflow
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.
I then create a new Subflow with a status node, one input and one output. This
Subflow optionally depends on the
Send Automation Speech Notification Subflow
.
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
}
Please note, for my Alexa speakers. I use a volume level between the range of
[0.0..0.10)
. To mute, pass0.0
.
Set Volume Level on All Media Devices Subflow
This subflow will get all media_player
' and then send them one by one to the
Set Volume Level
Subflow.
If you are not using Alexa, you may need to tweak the
Get All Media Players
node as it’s returning all devices that have a specific feature attribute which says I support changing the volume level.
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.
Normalize Volume Levels Everywhere Flow
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
).
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
}
]
NodeRed VS Home Assistant Automations
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.
Share this post
Twitter
Facebook
Reddit
LinkedIn
Email