Getting started with AppDaemon scripting

Hi everybody,

I recently got into AppDaemon and already love how much simpler it makes things. I would call myself a beginner at best when it comes to Python, yet I’d like to try writing a few simple scripts myself - which seems to be harder than I expected.

The ad-media-lights-sync script enables us to use “some” color from the currently playing track of a media_player entity as color for a light entity, which is pretty awesome. However, I do not like the way the script actually defines the color (hence I emphasized “some” color). For example, if the current song has a corresponding cover image that is mostly blue, but has some red parts in the center of the image, this script might use red as the color value, not blue. I would prefer to use the dominant color, even if it is not in the center of the image.

I wrote a little python script that will do just this

from colorthief import ColorThief
import sys

# for local testing: pass file to use as command line parameter
datei = sys.argv[1]

# global variable to use color values (ergebnis) outside the function
glob_ergebnis = 0

def klau_farbe():
    color_thief = ColorThief(datei)
    ergebnis = color_thief.get_color()
    global glob_ergebnis
    glob_ergebnis = ergebnis # could have written to this straight away, but writing this script was a process ;)

klau_farbe()
print(glob_ergebnis)

When I use this image (amazon direct link to image file), the script will output (31, 96, 77); this looks about right (though I actually expected something orange-ish instead of green/mint, but it is still a realistic value considering the original image).

Would somebody be so kind to get me started how I’d have to construct an AppDaemon script that will do the following (assuming similar configuration in apps.yaml as ad-media-light-sync):

  • query current cover image from media_player entity
  • run my script above on this image
  • write the output to something like sensor.klau_farbe (just as a test, later this value should be used directly via the appdaemon script and set the light entity accordingly)

Unfortunately, I cannot just read the linked script from above and know how to do this (as I said, python beginner here). It is too much and too complex (and yeah, I can see that it is only a few lines, but still :confused:) for me to adapt it to what I need.

But hopefully, if somebody can help me here, I can build on that and then create a script that will work similar to the one linked above. I would like to implement things that the creator of the original script is not planning on integrating in their script, so I am not trying to rebuild an existing solution - I want to modify this solution to my needs (of course I’d still share it if anybody is interested once it is done).

Thank you for your help :slight_smile:

I can post a simple example, but first you need to figure out how to install the colorthief python library, and they way to do this depends on how you are running Appdaemon, in a virtual environment, in Docker, or as an addon to Home Assistant (former hassio)?

1 Like

I currently run AppDaemon as a docker container, but am planning on switching to a virtual environment. Media-lights-sync required Pillow, which I installed by docker exec - it appdaemon_custom /bin/sh, then pip install Pillow. This ought to work similar with Color Thief.

Ok, here is a simple code:

I was looking to do something similar, but I didnt know about the colorthief library, so thanks for the tip!

  1. Create a file with the below content named colors.py and place it in the /conf/apps folder.
import appdaemon.plugins.hass.hassapi as hass
from colorthief import ColorThief
import urllib.request 


IMG_NAME = "img.jpg"


class Colors(hass.Hass):
    def initialize(self):

      # Get the config parameters from apps.yaml.
      self.sensor = self.args['sensor']
      self.media_player = self.args['media_player']
      self.light = self.args['light']

      # Listen for when the media player image changes.
      self.listen_state(self.set_color, self.media_player, attribute = "entity_picture")

    def set_color(self, entity, attribute, old, new, kwargs):
      # Retrieve the image url from the media_player attribute "entity_picture".
      img_url = self.get_state(entity, attribute="entity_picture")

      # Grab the image from the url and save it locally.
      urllib.request.urlretrieve(img_url, IMG_NAME)

      # Do the color stuff.
      color_thief = ColorThief(IMG_NAME) 
      rgb_color =  color_thief.get_color()
      rgb_list = [rgb_color[0],rgb_color[1],rgb_color[2]]

      # Update a sensor in HA with the new color.
      self.set_state(self.sensor, state=rgb_list)

      # Change the color of the light entity in HA.
      self.turn_on(self.light, rgb_color=rgb_list)
      
  1. Add the following to your apps.yaml file:
colors:
  module: colors
  class: Colors
  media_player: media_player.mibox4  # Or whatever name your media player has.
  sensor: sensor.my_color   # The name for  your sensor. The sensor will be created.
  light: light.my_light  # The name of a light entity
2 Likes

Thank you for your detailed instructions :slight_smile: I tried implementing them. I installed ColorThief via docker exec. When this script is triggered by changing the currently playing song, I get

2020-05-07 10:12:34.038985 WARNING AppDaemon: Unexpected error in worker for App ben_colorthief:
2020-05-07 10:12:34.039334 WARNING AppDaemon: Worker Ags: {'name': 'ben_colorthief', 'id': UUID('5c'), 'type': 'attr', 'function': <bound method Colors.set_color of <ben_colorthief.Colors object at 0x7f33202e1940>>, 'attribute': 'entity_picture', 'entity': 'media_player.jenkins_clementine', 'new_state': '/api/media_player_proxy/media_player.jenkins_clementine?token=abc&cache=5670', 'old_state': '/api/media_player_proxy/media_player.jenkins_clementine?token=abc&cache=10339', 'kwargs': {'attribute': 'entity_picture', 'handle': UUID('6')}}
2020-05-07 10:12:34.039942 WARNING AppDaemon: ------------------------------------------------------------
2020-05-07 10:12:34.046392 WARNING AppDaemon: Traceback (most recent call last):
  File "/usr/local/lib/python3.6/site-packages/appdaemon/appdaemon.py", line 595, in worker
    self.sanitize_state_kwargs(app, args["kwargs"]))
  File "/conf/apps/ben_colorthief/ben_colorthief.py", line 28, in set_color
    urllib.request.urlretrieve(img_url, IMG_NAME)
  File "/usr/local/lib/python3.6/urllib/request.py", line 248, in urlretrieve
    with contextlib.closing(urlopen(url, data)) as fp:
  File "/usr/local/lib/python3.6/urllib/request.py", line 223, in urlopen
    return opener.open(url, data, timeout)
  File "/usr/local/lib/python3.6/urllib/request.py", line 511, in open
    req = Request(fullurl, data)
  File "/usr/local/lib/python3.6/urllib/request.py", line 329, in __init__
    self.full_url = url
  File "/usr/local/lib/python3.6/urllib/request.py", line 355, in full_url
    self._parse()
  File "/usr/local/lib/python3.6/urllib/request.py", line 384, in _parse
    raise ValueError("unknown url type: %r" % self.full_url)
ValueError: unknown url type: '/api/media_player_proxy/media_player.jenkins_clementine?token=abc'
------------------------------------------------------------

Line 28 is urllib.request.urlretrieve(img_url, IMG_NAME). Except that line, all errors are caused by the imported libraries, if I understand the output correctly. Connection to Home Assistant must work, as changing songs triggers the script. The tracks I play do have covers (I can see this in lovelace). Do you know what causes this problem?

It looks like your media_player entity sets a relative url in the entity_picture attribute. My media players sets a valid URI. Would you mind posting the attributes of your media player from the state explorer page while it is playing something?

We might need to use the url of the HA instance + the relative path as the URI to grab the image.

Sure :

source_list:
  - Neue FAV
volume_level: 1
media_content_type: music
media_title: Bau keine Scheisse mit Bier
media_artist: Eisenpimmel
media_album_name: Bau keine Scheiße mit Bier!
source: Wiedergabeliste 1
friendly_name: '[Jenkins] Clementine'
entity_picture: >-
  /api/media_player_proxy/media_player.jenkins_clementine?token=967c&cache=12560
supported_features: 19509

I think I understand what you mean. I’ll have to prepend the path to the image; problem is that it seems like only Clementine (which I run on my local PC to test this) won’t provide the entire URL. Most other players are mpd, and I just played a song there - they do include the URL.

EDIT: nope! The mpd actually does not provide any URL now (though it will show an image in lovelace).

Ok, let’s just add a validation to check if the entity_picrure atttribute contains a valid URI. If so, we use it, if not, we prepend the url of your HA instance.

Try this: Pause your media player while it is playing.
Try to access this url in your browser:

http://192.168.x.x:8123/local/add_here_the_entity_picture_relative_path

or

http://192.168.x.x:8123/add_here_the_entity_picture_relative_path
1 Like

That works fine. The second URL you provided will show the image.

I didn’t want you to do all the work, so I changed

img_url = self.get_state(entity, attribute="entity_picture")

to

new_url = "http://url:8123" + entity_picture
img_url = self.get_state(new_url)

This did not work, but I realized where the error was, so I reverted my change and tried

urllib.request.urlretrieve(new_url, IMG_NAME)

But that will also not work (NameError: name "entity_picture" is not defined). I assume that I have to parse the URL Home Assistant provides with the actual part that is missing, just gotta figure out how to.

EDIT: Ha! I added new_url = "http://(...)" + img_url. Now I get what seems to be valid results :slight_smile: Thank you so much for your help. I’ll try to improve on this more, but now I have some understanding on how this works =)

Question though: you are using this as well, right. Will it work with multiple lights, or does this require changing the python file?

We can supple a list of media_players instead of just one in the apps.yaml config, and also a list of lights if you want to change more than one light entity:

colors:
  module: colors
  class: Colors
  media_players: 
    - media_player.mibox4 
    - media_player.other
  sensor: sensor.my_color 
  lights: 
    - light.my_light 
    - light.my_second_light

and add some looping in the code to handle the above.

1 Like

Awesome! Ok, last question for now :wink: Is it possible to also pass an effect? I tried

# (...)
self.effect = self.args['effect'] # added corresponding section to `apps.yaml`
# (...)
self.set_state(self.light, effect=effect) # didn't work
self.turn_on(self.light, rgb_color=rgb_list, effect=effect) # neither

I am pretty sure this can be done, but I didn’t find information on what can be passed to the light entity, and how. It’d be cool to add effect and brightness; for effect, I’d randomly select one of the effects that work with manually set colors only.

Okay, getting closer:

self.mein_effect = self.args['effect']

self.set_state(self.light, attributes={"effect": self.mein_effect})

This will change the effect for a second, then it will revert to it’s previous value. I had Android as my WLED effect. In apps.yaml, I added

ct:
  module: ct
  class: Ct
  media_player : ...
  sensor : ...
  light: ...
  effect : Breathe

This will work, but ultimately change again from Breathe to Android. I’ll keep working on it…

Try this: Replace the self.turn_on with:

self.call_service("light/turn_on", entity_id=self.light, effect="colorloop")
1 Like

Damn, I got a lot to learn. Thank you so much. It works now in a way that’ll allow me to build up on it on my own, hopefully :slight_smile:

Great!

Here is my app so far, with comments:

import appdaemon.plugins.hass.hassapi as hass
from colorthief import ColorThief
import urllib.request 

IMG_NAME = "img.jpg"

class Colors(hass.Hass):
    def initialize(self):
      
      # Get the HA url from the appdaemon.yaml file.
      self.base_urls = self.find_key("ha_url",self.config)
      self.base_url = ""
      for self.base_url in self.base_urls:
        pass
      
      # Get the config parameters from apps.yaml.
      self.sensor = self.args['sensor']
      self.media_player = self.args['media_player']
      self.lights = self.args['lights']

      # Listen for when the media player image changes.
      self.listen_state(self.set_color, self.media_player, attribute = "entity_picture")

    def set_color(self, entity, attribute, old, new, kwargs):
      # Retrieve the image url from the media_player attribute "entity_picture".
      img_url = self.get_state(entity, attribute="entity_picture")

      # Try to grab the image from the url and save it locally.
      try:
        # First, try with base url and relative picture_entity url.
        urllib.request.urlretrieve(self.base_url + img_url, IMG_NAME)
      except:
        # if the above fails, try with just picture_entity url.
        urllib.request.urlretrieve(img_url, IMG_NAME)
  
      # Get the dominant rgb values into a list.
      color_thief = ColorThief(IMG_NAME) 
      rgb_color =  color_thief.get_color()
      rgb_list = [rgb_color[0],rgb_color[1],rgb_color[2]]

      # Update the sensor in HA with the current dominant color.
      self.set_state(self.sensor, state=rgb_list)

      # Change the color of the lights entities in HA.
      for light in self.lights:
        if "effect" in self.args:
          self.call_service("light/turn_on", entity_id=light, rgb_color=rgb_list, effect=self.args["effect"])
        else:
          self.call_service("light/turn_on", rgb_color=rgb_list, entity_id=light, effect="")

    def find_key(self,key, dictionary):
        for k, v in dictionary.items():
            if k == key:
                yield v
            elif isinstance(v, dict):
                for result in self.find_key(key, v):
                    yield result
            elif isinstance(v, list):
                for d in v:
                    if isinstance(d, dict):
                        for result in self.find_key(key, d):
                            yield result
      

and the corresponding app.yaml entries:

media_player_one:
  module: colors
  class: Colors
  media_player: media_player.mibox4_2
  sensor: sensor.media_player_one_color
  lights: 
    - light.taklampan
    - light.bordslampor
  effect: random  # This is optional.

media_player_two:
  module: colors
  class: Colors
  media_player: media_player.livingroom
  sensor: sensor.media_player_two_color
  lights: 
    - light.livingroom
    - light.table_lamp

etc...
1 Like

I don’t quite understand your find_key function; is it so that you don’t have to specify media_player, lights etc. by hand?

Also, unfortunately I am not at the state where I can use try, except, pass etc; guess my script is likely do break while yours will handle errors in a decent way, right?

I got ben_colorthief.py (sorry for comments in German; this was supposed to be my “testing” app)

# Interaktion zwischen Home Assistant und AppDaemon
import appdaemon.plugins.hass.hassapi as hass
# Farben auslesen
from colorthief import ColorThief
# HTTP Kram
import urllib.request
# Zufallsauswahl
import random

# VARIABELN FESTLEGEN
IMG_NAME = "img.jpg"

# Klasse zum Auslesen der Farbe
class BenColorThief(hass.Hass):
    def initialize(self):

        # erfrage die Config Parameters aus apps.yaml
        self.sensor = self.args['sensor']
        self.media_player = self.args['media_player']
        self.light = self.args['light']
        self.mein_effect = self.args['effect']
        self.url = self.args['ha_url']
        # self.helligkeit = self.args['brightness']

        # Beobachte, wann der media_player die Grafik ändert
        self.listen_state(self.set_color, self.media_player, attribute="entity_picture")

    def set_color(self, entity, attribute, old, new, kwargs):
        # erhalte URL der Grafik vom media_player attribute "entity_picture"
        img_url = self.get_state(entity, attribute="entity_picture")
        new_url = self.url + img_url

        # Speichere die Grafik lokal ab
        urllib.request.urlretrieve(new_url, IMG_NAME)

        # Farben auslesen, festlegen, etc.
        color_thief = ColorThief(IMG_NAME)
        rgb_color = color_thief.get_color()
        rgb_list = [rgb_color[0], rgb_color[1], rgb_color[2]]

        # Update den Sensor in Home Assistant mit der neuen Farbe
        self.set_state(self.sensor, state=rgb_list)

        # Zufälligen Effekt beim Wechsel
        optionen = self.mein_effect
        zufall = random.choice(optionen)

        # Stelle das Licht entsprechend ein
        self.turn_on(self.light, rgb_color=rgb_list)
        # Verändere den Effekt und die Helligkeit
        # self.call_service("light/turn_on", entity_id=self.light, effect=self.mein_effect, brightness=self.helligkeit)
        self.call_service("light/turn_on", entity_id=self.light, effect=zufall)

        # Ab hier rumgeteste
        # teststate = self.get_state(self.light, attribute="effect")
        # self.log("Hier steht was")
        # self.log(teststate)

an in apps.yaml

ben_colorthief:
  module: ben_colorthief
  class: BenColorThief
  # Home Assistant URL muss manuell übergeben werden
  ha_url: !secrets ha_url
  # Welcher Mediaplayer soll gesteuert werden?
  media_player: media_player.jenkins_clementine
  # Sensor enthält die ausgelesenen Werte; der Sensor wird automatisch erstellt
  sensor: sensor.colorthief
  # Welches Licht soll kontrolliert werden?
  light: light.wled_test_light
  # Effekt ebenfalls festlegen
  effect : 
    - Android
    - Breathe
    - Bouncing Balls
    - Candle
    - Chase
    - Chase Flash
    - Drip
    - Fade
    - Fire Flicker
    - Fireworks
    - Heartbeat
    - Lighthouse
    - Meteor
    - Multi Comet
    - Plasma
    - Popcorn
    - Rain
    - Ripple
    - Running
    - Running 2
    - Saw
    - Scan
    - Scan Dual
    - Sinelon
    - Sinelon Dual
    - Sparkle
    - Spots
    - Spots Fade
    - Stream
    - Sweep
    - Sweep Random
    - Theater
    - Tri Chase
    - Tri Fade
    - Tri Wipe
    - Twinklecat
    - Twinklefox
    - Two Areas
    - Two Dots
  # Helligkeit festlegen
  # brightness: 50

I manually tested all WLED effects to find those that seem to work with a set color (unlike Halloween, for example, that will always use green and orange), and a random effect will be chosen whenever there is a change in the currently played song.

While my solution is probably spaghetti code, it works for now; the only big issue I have is that while it works with Clementine, none of the mpd types of media_player will provide a URL for the cover image. However, it is there (will be displayed in my lovelace UI when a cover is embedded in the file, or if cover.png|jpg exists). So I’ll have to figure out how to get the appropriate URL. Will you receive the appropriate cover image regardless of the media player you are using?

Btw., @tjntomas how content are you with the overall colors so far? I am currently considering to add an option to turn anything that is too white (or any color that is bright, but not very saturated) either to a random effect, or a specific color. I didn’t realize I had so many albums that have too much white in them, so whenever those play, the LED look kind of boring :wink:

Yes, the colors need some fixing for sure. There is a library to help with this that can saturate and desaturate colors, and keep visual brightness constant since some colors are brighter than others. I just need to remember the name of it.

Yes, I always get a valid URL. Do you get an attribute named entity_picture_local in your media_player? You might need to check if the local picture is available and prepend the ha url when available.

The find_key function just looks up the Home Assistant base url from the appdaemon config so I dont have to enter it manually.

You should always use try/catch with all code that does any form of i/o.

There is colour; I just tested it like this

#!/usr/bin/env python3

'''
automatisch zu helle Farben "verstärken";
basiert auf "colour" library
'''

from colour import Color

testfarbe = [131, 98, 72]
c0 = '#%02x%02x%02x' % (testfarbe[0], testfarbe[1], testfarbe[2])
c = Color(c0)

print(c.hex)
c.saturation = 1
print(c)

I used [131, 98, 72] as a test color, which is a brownish color, but actually (if I understand the output correctly) a very de-saturated orange.

Before modifying saturation, it will be hex #836248, after hex #cb5900. While this is fine, it would be much better if one didn’t have to manually convert it. ==> While typing this, I also found colormap, which will do this

from colormap import rgb2hex

testfarbe = [131, 98, 72]

print(rgb2hex(testfarbe[0], testfarbe[1], testfarbe[2]))

Output will also be #836248. So I guess we can combine both libraries. Am I correct to assume that we’ll also need some method to define the actual saturation value? For example, #836248 is some de-saturated value of #cb5900. Something like (not actual code)

current = "#836248"

mysat = get_saturation_value(current)  # for example, 0.3 percent

if mysat <= 0.6
  # saturate it more
   pass

I will look into this more throughout the day. Oh, and thank you! I included the key function as well to automatically detect the home assistant url.

update:

#!/usr/bin/env python3

'''
automatisch zu helle Farben "verstärken";
basiert auf "colour" library
'''

from colour import Color
from colormap import rgb2hex, hex2rgb

testcolor = [131, 98, 72]

print("Color in RGB:", testcolor)
c = Color(rgb2hex(testcolor[0], testcolor[1], testcolor[2]))
print("Color in hex:", c.hex)
print("Current saturation:", c.saturation)

c.saturation = 1
print("Color fully saturated:", c)

c = hex2rgb("%s" %c)
print("Finally, color in RGB:" , c)

Is this going in the right direction?

Looks like you are onto something useful. I’ll have a look at those two libraries and report back any progress.