Set RGB Bulb/LED Strip to match Spotify/Kodi Media Artwork [Node-RED/Alexa]

I used to use This Project but after I stopped using App Daemon and moved to Node-RED I have been meaning to get this up and running again. I came across This on reddit and it gave me the inspiration to get in and try to make this work with Alexa as I no longer use Chromecast for Spotify streaming.

The following is using Node-RED connected to Home Assistant and uses an external Library called color thief to get the prominent colour from a url image.

Steps:

  • SSH into your machine/container running node-red

  • Install color-thief-node npm i color-thief-node

  • Ensure your nodered/settings.js contains a block to import the new color-thief-node library:

       functionGlobalContext: {
      	ColorThief:require('color-thief-node')
          },
    

Now, in Node-Red

  • Start a flow with the State Changed HA node. Set it to target the Media Player you want to monitor (e.g. media_player.echo_1)
  • Connect this node to a Current State node with the same entity_id (this will form part of the loop we create later on)
  • Then connects this with a switch node with the following 2 outputs of ā€œplayingā€ & ā€œpausedā€

image

  • Connect this node to a Function node with the following code replacing the section for imageURL with your own instance details:

      //Only run if Music is the active content
      if (msg.payload == "playing"){
    
          //Import the ColorThief module from the alias we set in Settings.js
          const { getColorFromURL } = global.get('ColorThief');
          
          //Get the file path to the Alexa's currently playing track out of the msg data from our first node
          imageURL = "YOURPUBLICIP:YOURPORT" + msg.data.attributes.entity_picture;
    
          //get the colour from the image we found. This function is asynchronous, which is why we need the .then function, which acts once the result is available and then forward the data on to the rest of the flow 
    
          const mainColor = getColorFromURL(imageURL) .then(values => { 
      node.send({payload:values}); }); }
    
          //Because this flow is asynchronous, we do not return the message object, we just call return; the message object gets sent by node.send() once the async call completes
          return;
    

These next steps are directly from the reddit post and I liked what he did you can either use or not its up to you.

  • You now have the dominant color of the current album art as an array of RGB values. For me though, they werenā€™t quite rich enough, so I added a few steps before setting the bulb:

  • In Node-Red, go to Menu>Manage Palette>Install and install node-red-contrib-color-convert

  • Add a color convert node to our flow, from RGB to HSV or HSL

  • Follow on with the following code in another function node:

      //These 3 lines boost the saturation, relative to how de-saturated a colour is
      satDiff = 100 - msg.payload[1];
      saturation = msg.payload[1] + (satDiff /2);
      //But don't let the saturation be > 100 else HA with throw an error
      if (saturation > 100) {saturation=100;}
    
      //Construct a payload to send to the light using our values
      msg.payload = 
      {
        "data":
        {
          "entity_id" : "light.color_bulb",
          "brightness": "255",
          "hs_color": [msg.payload[0], saturation]
        }
      }
      return msg;
    

where light.color_bulb is the entity name of your color lightbulb in home-assistant. You may wish to consider a different brightness. If you want to leave the saturation values alone, ignore the lines before msg.payload, and replace saturation in the hs_color with msg.payload[1].

  • Lastly, use a call service node to call light.turn_on. Leave the data field blank, as it will be filled with our payload.

The problem I encountered from here is that the URL will not change between songs as the function as it stands only acts on the state of the media_player is playing. In order to force the URL to update for each song we need to add the following loop into the flow.

Off the ā€œplayingā€ output of our switch node created earlier we need to create 2 functions one to get the current song length and one to get the current song position.

  • create a function node with the following code to retrieve the total song time in secs:

      newmsg = {};
      if (msg.payload == "playing") {
          newmsg.payload = msg.data.attributes.media_duration;
      } else {
          newmsg.payload = "False";
      }
    
      return newmsg;
    
  • now create a second function node to get the current position in time of the playing track:

      newmsg = {};
      if (msg.payload == "playing") {
          newmsg.payload = msg.data.attributes.media_position;
      } else {
          newmsg.payload = "False";
      }
    
      return newmsg;
    
  • Now we need to create a join node to bring these msg together set the mode - manual to create an Array.

  • next connect to a function node in the function we will subtract the current playing position from the total song time to get when to set our delay to poll for the next artwork URL:

      var contA= msg.payload[0];
      var contB= msg.payload[1];
    
      msg.payload = (contA - contB);
    
      return msg
    
  • Now we need to put the output into a change node to add the msg.topic of delay so we can variably set the following delay Node to the length of the song remaining ( basically being smart about polling only when a new song starts to get the new URL RGB Colours)

This should all look like this

Lastly you could act on the ā€œpausedā€ output of the switch node to turn off the lights or set them back to normal settings. I use a time range to determine whether I turn the lights off or set them back to there normal colour.

To use this for Kodi replace with the media_player id of your Kodiand it will essentially do the same thing but for any artwork so you can sync it with TV and Movie Posters also.

Thanks for reading.

3 Likes

Thank you for that! Iā€™m running Hassio in Docker and Iā€™m not able to install npm packages via ssh - nevertheless I found a solution to this. I thought I might write it down for anyone who is using a dockerized Hassio as well.

Edit the config of your Node-Red to include these system and npm packages:

  "system_packages": [
    "build-base",
    "g++",
    "cairo-dev",
    "jpeg-dev",
    "pango-dev",
    "imagemagick"
  ],
  "npm_packages": [
    "fs-minipass",
    "minizlib",
    "color-thief-node"
  ],

Iā€™m using the Spotify integration, which means my first event state note looks like this:


Note that I unchecked ā€œOutput only on state changeā€. So every time something in the attributes changes, a new event will fire - with this Iā€™m able to skip the workaround of OP with getting the song time and current position, as a new event will fire every time a new song is being played.

Because youā€™re not able to edit the settings.js of Node Red if you installed it via the add-on store, I used a function-npm node instead of a ā€œnormalā€ function node. Install it via settings -> manage palettes -> install -> ā€œnode-red-contrib-function-npmā€.

Create a function npm node with the following, slightly modified code:

const { getColorFromURL } = require('color-thief-node');
 
//Only run if Spotify is running on my TV
if (msg.data.new_state.attributes.source == "[LG] webOS TV UK6200PLB"){


    //Get the file path to the Spotify currently playing track out of the msg data from our first node
    imageURL = "https://SUBDOMAIN.DOMAIN.com" + msg.data.new_state.attributes.entity_picture;

    //get the colour from the image we found. This function is asynchronous, which is why we need the .then function, which acts once the result is available and then forward the data on to the rest of the flow 

    const mainColor = getColorFromURL(imageURL) .then(values => { 
node.send({payload:values}); }); }

    //Because this flow is asynchronous, we do not return the message object, we just call return; the message object gets sent by node.send() once the async call completes
    return;

Note that I modified the code to only run if Spotify is currently playing on my TV.

Because Iā€™m using Hue Play Lightbars, Iā€™m able to use the RGB values directly, so I created a second function:

  //Construct a payload to send to the light using our values
  msg.payload = 
  {
    "data":
    {
      "brightness": "255",
      "rgb_color": [msg.payload[0], msg.payload[1], msg.payload[2]]
    }
  }
  return msg;

Now we only need to create a call service node with the entities which should show the most dominant color of the Spotify album art.

The overall flow looks like this:

With this my Hue Play Lights will show the same color as album art of the currently playing song (looks a bit better IRL as the light is a bit softer than seen in the picture):

But sadly the Spotify app isnā€™t displaying the most dominant color butā€¦ something different. Resulting in this:

I found a python script which gets the background color of Spotify correct at 80% of the time (https://github.com/davidkrantz/Colorfy/blob/1bc96dc4ce619dcc6df6ed2be6d88c7fc1947e9e/spotify_background_color.py).

I might include this when I find the time, nevertheless still looks pretty cool at this stage. Thank you jimpower for this awesome idea and your code!

1 Like

@josef Were did you find this list of dependencies?

@DaanWijffels I found it in this reddit discussion about this topic:

I also tested it without some of this packages - didnā€™t work until I included all of them. You can also work your way forward as NodeRed is complaining if there are unmet dependencies and which packages are missing.

@josef
Hi, I appreciate all the work you did, to get this working, but I cant figure out where the paste or change the system_packages and the npm_packages in the config file. they arenā€™t there already and donā€™t know in what section to paste those. Hope you can help me out. I try this, cause I canā€™t install it wit hss (running in a pi)
Thanks in advance.

Hey @Akylanator, no worries. Some things have changed and my description is a bit outdated anyway.

The config file is right under the add-on details for Node-Red. But you need some packages additional to those I posted. Also, the format of the config changed from json to yaml.

Just copy this into the settings of node-red.

system_packages:
  - musl
  - build-base
  - make
  - g++
  - cairo-dev
  - jpeg-dev
  - pango-dev
  - imagemagick
npm_packages:
  - fs-minipass
  - minizlib
  - color-thief-node

@josef Thank you for your quick response! :slight_smile:

@josef Hi, I got an other question about this, I did manage to install the npn packages and I think they work. But when I try to run npn function node, this debug message pops up: ā€œTypeError: Cannot read property ā€˜entity_pictureā€™ of undefinedā€. I did connect a Google Nest Hub to the state node and edited my credentials in the npn node (I changed it to ā€œhttps://mydomain.duckdns.orgā€). Is there something that iā€™m missing? Or doesnā€™t the Nest Hub send out the propriate message values? Anyhow to solve this? I donā€™t have spotify premium so I cant setup the spotify integration. Thanks for helping me out

Itā€™s important that the entity of the state node you included does have a property called entity_picture. You can check the properties of all devices in Home Asisstant via ā€œdeveloper toolsā€ > ā€œstatesā€.

Compare it to the attached screenshot (everything in german but you should see what I mean)

I for example could also use the media_player.bad_dot as it also uses the attribute entity_picture (in this case, itā€™s an Amazon Alexa Device and I wouldnā€™t need to add my domain as itā€™s already a public domain). Check if Google Nest Hub does have an entity_picture attribute as well or else, check if there is a media_player entity of Google Nest Hub which maybe includes entity_picture.


I got a entity_picture url, but how do i set this up in the node. Whats the correct way to make the imageURL ( I donā€™t know what to fill in between the brackets). thanks for replying so fast

Ah, great. As itā€™s a public url, you should use this as the content of your function node:

const { getColorFromURL } = require('color-thief-node');
 
//Only run if Spotify is running on my TV
if (msg.data.new_state.attributes.source == "[LG] webOS TV UK6200PLB"){


    //Get the file path to the Spotify currently playing track out of the msg data from our first node
    imageURL = msg.data.new_state.attributes.entity_picture;

    //get the colour from the image we found. This function is asynchronous, which is why we need the .then function, which acts once the result is available and then forward the data on to the rest of the flow 

    const mainColor = getColorFromURL(imageURL) .then(values => { 
node.send({payload:values}); }); }

    //Because this flow is asynchronous, we do not return the message object, we just call return; the message object gets sent by node.send() once the async call completes
    return;

You donā€™t need to paste your duckdns address before that as the entity picture url isnā€™t a relative but an absolute URL. Feel free to ask if thereā€™s still problems. Happy to help.

Edit:
You could also use the entity_picture_local but than you would need your DUCKDNS address posted before that string as itā€™s a relative URL.

Edit2: Iā€™m sorry, for my setting I inluded an if statement to check if spotify is running on my tv.

Use that Code instead:

  //Only run if Music is the active content
  if (msg.payload == "playing"){

      //Import the ColorThief module from the alias we set in Settings.js
      const { getColorFromURL } = global.get('ColorThief');
      
      //Get the file path to the Alexa's currently playing track out of the msg data from our first node
      imageURL = + msg.data.attributes.entity_picture;

      //get the colour from the image we found. This function is asynchronous, which is why we need the .then function, which acts once the result is available and then forward the data on to the rest of the flow 

      const mainColor = getColorFromURL(imageURL) .then(values => { 
  node.send({payload:values}); }); }

      //Because this flow is asynchronous, we do not return the message object, we just call return; the message object gets sent by node.send() once the async call completes
      return;

But with this itā€™s changing the colors of your light everytime a new picture is set to entity picture. Even if itā€™s not spotify.

So I tried a lot of other things and I finaly got me a working flow.
I deleted node red and did a fresh install.
I added the following in the configuration section of the node red adon:

  "system_packages": [
    "build-base",
    "g++",
    "cairo-dev",
    "jpeg-dev",
    "pango-dev",
    "imagemagick"
  ],
  "npm_packages": [
    "fs-minipass",
    "minizlib",
    "color-thief-node"
  ],

Then I installed the ā€œnode-red-contrib-function-npmā€ via the manga palettes -> install.
I restarted node red (it took a while (10 min) to come back online), and after that I made a function-npm node with the following:

if (msg.payload == "playing"){

    //Import the ColorThief module from the alias we set in Settings.js
    const { getColorFromURL } = require('color-thief-node');
    
    //Get the file path to the Chromecast's currently playing track out of the msg data from our first node
    imageURL = msg.data.new_state.attributes.entity_picture;

    //get the colour from the image we found. This function is asynchronous, which is why we need the .then function, which acts once the result is available and then forward the data on to the rest of the flow 

    const mainColor = getColorFromURL(imageURL) .then(values => { 
node.send({payload:values}); }); }

    //Because this flow is asynchronous, we do not return the message object, we just call return; the message object gets sent by node.send() once the async call completes
    return;

I hooked up the other function and call node and it worked flawlessly.

1 Like

Hello,

Iā€™m trying to follow your advice, I have managed to create all the nodes however it seems it isnā€™t able to ready the cover picture.

I see you are talking about a URL in image URL, however iā€™m not sure if I have missed something but Iā€™m not sure what to put as domain and subdomainā€¦

imageURL = "https://SUBDOMAIN.DOMAIN.com" + msg.data.new_state.attributes.entity_picture;

I am trying to get the cover picture from Spotify.

Thanks in advance!

Nevermind!

Worked with ā€œhttp://127.0.0.1:8123ā€ ! Thanks for all this guys!

Hello there,

I think it is cheating, but I found a way for the colours to be more ā€œreliableā€ according to Spotifyā€™s taking on the background colour.

Instead of trying to have a more reliable processing of the covers, I am doing it this way:

When you link your Android device (in my case my Nvidia Shield) to HA, the entity-picture becomes litterally a ā€œscreenshotā€ of the screen.

So I amended the source to be ā€œmedia_player.nvidia_shieldā€ in the first state node, then in the second node (the npm function) I have put the source to be ā€œSpotifyā€ so that it only triggers when I am listening to music from the Shield.

And since the background colour represents most of the screen, it is the colour grabbed!

Bonus point: This way you donā€™t encounter Spotifyā€™s ā€œupdate lagā€!