Show Grafana Graphs in Home-Assistant

This is a Proof of Concept!

I tried to display grafana graphs in home-assistant and found a solution.

Step 1: Grafana is able to render PNG files from a Panel via API

For this purpose you have to share a panel and use the link “Direct link rendered image” as shown in the following image

The resulting link looks like this:

`http://[User]:[Password]@[IP or hostname of grafana]:3000/render/dashboard-solo/db/aussenbedingungen?orgId=2&panelId=1&theme=light&width=500&height=250`

You have to add [User] and your [Password] to access grafana. You cann adapt width and height at will. The theme is something you can adjust by the dialog shown above.

The resulting image looks like this (colors for graphs can be different - you can define them in grafana):

Step 2 How to get these images to use them in home-assistant

Therefore I used the downloader component and had to adapt it. I duplicated the original downloader component and created a new component with a new domain “grafana_graphs” in the directory “custom_components/”.

Why? Because the original downloader component does not generate a usable filename if query string is given in the url - which is the case. Further, the original downloader component does not remove a file but create a new file and append a sequential number to it. Both are not useable in this situation. Further I have to use the OrgId and PanelId from the url to append it to the filename. Otherwise the filename would be the same for each panel in a dashboard. So the filename is generated in the format [dashboard name]__[dashboard number = OrgId form url]__panel number in the dashboard = PanelId from url]. For example “aussenbedingugen_2_1”. The downloader component can download the file into a specific directory and - if defined - subdirectory.

Here is the file [configdir]/custom_components/grafana_graphs.py:

"""
This is my own downloader component, because the original downloader does not handle filename
from url in a correct way and does not remove the file before fetching. 

Original: 
Support for functionality to download files.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/downloader/
"""
import logging
import os
import re
import threading

import requests
import voluptuous as vol

import homeassistant.helpers.config_validation as cv
from homeassistant.util import sanitize_filename

_LOGGER = logging.getLogger(__name__)

ATTR_SUBDIR = 'subdir'
ATTR_URL = 'url'

CONF_DOWNLOAD_DIR = 'download_dir'

DOMAIN = 'grafana_graphs'

SERVICE_DOWNLOAD_FILE = 'download_file'

SERVICE_DOWNLOAD_FILE_SCHEMA = vol.Schema({
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_SUBDIR): cv.string,
})

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
    vol.Required(CONF_DOWNLOAD_DIR): cv.string,
}),
}, extra=vol.ALLOW_EXTRA)


def setup(hass, config):
"""Listen for download events to download files."""
download_path = config[DOMAIN][CONF_DOWNLOAD_DIR]

# If path is relative, we assume relative to HASS config dir
if not os.path.isabs(download_path):
    download_path = hass.config.path(download_path)

if not os.path.isdir(download_path):
    _LOGGER.error(
        "Download path %s does not exist. File Downloader not active",
        download_path)

    return False

def download_file(service):
    """Start thread to download file specified in the URL."""
    def do_download():
        """Download the file."""
        try:
            url = service.data[ATTR_URL]

            subdir = service.data.get(ATTR_SUBDIR)

            if subdir:
                subdir = sanitize_filename(subdir)

            final_path = None

            req = requests.get(url, stream=True, timeout=10)

            if req.status_code == 200:
                filename = None

                if 'content-disposition' in req.headers:
                    match = re.findall(r"filename=(\S+)",
                                       req.headers['content-disposition'])

                    if match:
                        filename = match[0].strip("'\" ")

                if not filename:
                    file_name = os.path.basename(url).split('/')[-1].split('?')[0]
                    orgid = os.path.basename(url).split('?')[1].split('&')[0].split('=')[1]
                    panelid = os.path.basename(url).split('&')[1].split('=')[1]
                    filename = "{}_{}_{}".format(file_name, orgid, panelid)

                if not filename:
                    filename = 'ha_download'

                # Remove stuff to ruin paths
                filename = sanitize_filename(filename)

                # Do we want to download to subdir, create if needed
                if subdir:
                    subdir_path = os.path.join(download_path, subdir)

                    # Ensure subdir exist
                    if not os.path.isdir(subdir_path):
                        os.makedirs(subdir_path)

                    final_path = os.path.join(subdir_path, filename)

                else:
                    final_path = os.path.join(download_path, filename)

                path, ext = os.path.splitext(final_path)

                ### If file exist append a number.
                ### We test filename, filename_2..
                ### tries = 1
                ### final_path = path + ext
                ### while os.path.isfile(final_path):
                ###     tries += 1
                ###     final_path = "{}_{}.{}".format(path, tries, ext)

                # Remove file 
                if final_path and os.path.isfile(final_path):
                    os.remove(final_path)
                
                _LOGGER.info("%s -> %s", url, final_path)

                with open(final_path, 'wb') as fil:
                    for chunk in req.iter_content(1024):
                        fil.write(chunk)

                _LOGGER.info("Downloading of %s done", url)

        except requests.exceptions.ConnectionError:
            _LOGGER.exception("ConnectionError occurred for %s", url)

            # Remove file if we started downloading but failed
            if final_path and os.path.isfile(final_path):
                os.remove(final_path)

    threading.Thread(target=do_download).start()

hass.services.register(DOMAIN, SERVICE_DOWNLOAD_FILE, download_file,
                       schema=SERVICE_DOWNLOAD_FILE_SCHEMA)

return True

Next I created a python_script in “[configdir]/python_scripts” which is using this component and fetching the images via API from grafana. The used urls are generated by Step 1. In this example I fetch 3 images:

"""
Download a rendered png file from a panel in grafana into a subdirectory.
Please note: This requires a custom_component grafana_graphs.py.
"""

subdir_name = 'grafana_graphs'

# Außenbedingungen
data = {
    'url': 'http://[User]:[Password|@[IP or hostname of grafana]:3000/render/dashboard-solo/db/aussenbedingungen?orgId=2&panelId=1&theme=light&width=500&height=250',
    'subdir': subdir_name,
}

# call custom grafana_graphs component to save the file
hass.services.call('grafana_graphs', 'download_file', data)

# Bad Stromverbrauch Deckenlicht
data = {
    'url': 'http://[User]:[Password|@[IP or hostname of grafana]:3000/render/dashboard-solo/db/bad?orgId=2&panelId=4&theme=light&width=500&height=250',
    'subdir': subdir_name,
}

# call custom grafana_graphs component to save the file
hass.services.call('grafana_graphs', 'download_file', data)

# Bad Stromverbrauch Spiegellicht
data = {
    'url': 'http://[User]:[Password|@[IP or hostname of grafana]:3000/render/dashboard-solo/db/bad?orgId=2&panelId=5&theme=light&width=500&height=250',
    'subdir': subdir_name,
}

# call custom grafana_graphs component to save the file
hass.services.call('grafana_graphs', 'download_file', data)

Please note: I want to create the files in the sub-directory “grafana_graphs”.

Step 3: Configuration and automation

The custom component has to be configured in configuration.yaml. You have to specify the directory where the files should be created. In this example I created a subfolder “downloads” in the configuration directory. A sub-directory is optionally defined in the python_script above!

# Downloader component for grafana graphs
grafana_graphs:
  download_dir: downloads

Next, you can define a camera device for a local_file:

camera:
  - platform: local_file
    name: Temperaturen
    file_path: downloads/grafana_graphs/aussenbedingungen_2_1

Please remember the filename. In this example “aussenbedingungen” is the name of the dashboard. “2” is the number of the dashboard (OrgId in the url) and “1” is the number of the panel in that dashboard "PanelId in the url).

Next, you have to define where to display the camera device. In my example I display it on the default_view:

default_view:
  view: yes
  name: Home
  entities:
    - sun.sun
    - camera.temperaturen

And finally, you have to create an automation to fetch the images repeatedly:

- alias: Fetch graphs from grafana
  initial_state: On
  trigger:
    platform: time
    minutes: '/10'
    seconds: '00'
  action:
    service: python_script.retrieve_grafana

In this example I fetch the graphs every 10 minutes.

Result

This is a screenshot with the integrated graph from grafana.

An this is the detail view:

Final notes

As I mentioned in the beginning - this is a proof of conecpt. I know it would be better to program a component which fetchs the images directly via api from grafana. But my python skills are to little to do that.

I did this proof of conecpt since some people asked for the possibilty to use grafana graphs - some time ago in this forum.

It would be very good, if someone could create a offical component for that.

17 Likes

Great work! Thanks for sharing.

A friendly alert. Your grafana_graphs.py script is not working. The service is not registering,
A lot of problems with the indentation.

1 Like

Any thoughts about contributing this to the core distribution? I think it would be really helpful.

Hi,

I followed up exactly what you write, but keep getting the following error in the logs:
Could not read camera image from file: downloads/grafana_graphs/xxxx

Changed a lot in indentation in your python scripts. Scripts does not give any errors now, but not grapsh shown in the folder.

Could you help?
Thanks

The offical downloader component now allows to specify a filename and whether to overwrite the file or not. Have a look at https://home-assistant.io/components/downloader/.

You therefore no longer have to create a custom component like in step 2 shown.

If you created a custom component as described in step 2 in the original post you can delete your [configdir]/custom_components/grafana_graphs.py .

Your python_script [configdir]/python_scripts/retrieve_grafana.py now should look like:

"""
Download a rendered png from a panel in grafana into a subdirectory.
"""

subdir_name = 'grafana_graphs'

# example parameters for downloader component
data = {
    'url': 'http://[User]:[Password]@[IP or hostname of grafana]:3000/render/dashboard-solo/db/aussenbedingungen?orgId=2&panelId=1&theme=light&width=500&height=250',
    'subdir': subdir_name,
    'filename': 'aussenbedingungen_2_1',
    'overwrite': True
}

# call  downloader component to save the file
hass.services.call('downloader', 'download_file', data)

Please be sure to replace [User], [Password] and [IP or hostname of grafana] to your need.

In the script you can see that a filename can be specified e.g. 'filename': 'aussenbedingungen_2_1' and the option 'overwrite': True can be specified to overwrite the file if existing.

Other than in step 3 you have to put

downloader:
  download_dir: downloads

in your configuration.yaml.

That’s it.

1 Like

Thanks for your quick response! Did what you said, but i keep getting in the log:
2017-12-19 23:40:00 WARNING (MainThread) [homeassistant.core] Unable to find service python_script/retrieve_grafana

Also running downloader.download_file from the HA page does not work. Also errors that the camera file cannot be found.

It looks like the error message indicates that there is no file/script retrieve_grafana.

Please check whether your script

retrieve_grafana.py

is in your

python_scripts

directory?

And check whether

python_scripts

directory is in the folder where your

configuration.yaml

is.

Yes, this is both the case. Checked double.
Chmod 777 on the file and dir gives no result either.

Can it possibly be that you didn’t enable pyhton_scripts in your configuration.yaml?

# Allow to use python_scripts
python_script:

See https://home-assistant.io/components/python_script/

Can’t imaging that i overlooked this setting. The error 'cannot find service ’ didn’t triggered me. Thanks !!!
it is working like a charme now.

How did you get this to work? Grafana says there is no url based authentication. How to auth via browser URL without enabling anonymous? · Issue #2729 · grafana/grafana · GitHub

I am also not able to get the image on the URL unless I enter the username/password again

http://[User]:[Password]@192.168.2.113:3000/render/dashboard-solo/db/home-assistant?refresh=5s&panelId=1&orgId=1&from=1517756914526&to=1517778514526&width=1000&height=500&tz=UTC-05%3A00

When I paste the URL in Firefox, I see the message:

You are about to log in to the site “192.168.2.113” with the username “root”, but the website does not require authentication. This may be an attempt to trick you.

Is “192.168.2.113” the site you want to visit?

Looks like we need to use API keys to get the images. See here.

Now, we need to figure out how to save the file in the download directory.

Please look at http://docs.grafana.org/http_api/auth/ under “Basic Auth” an be aware that a browser is handling the auth for security reasons different then e.g. curl or a python script. For example see “Access using credentials in the URL” under https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication.

oh okay, thanks for the link, that explains it pretty well. will definitely try over the weekend.

Posted details of the API key and URL here.

1 Like

Agreed!! Ll

I have problem with render this image in Home Assistant.

retrieve_grafana.py create file but it’s not png file.

So in HA i have (image not available)

In webbrowser link work.

So i need use link with login and password in ha but in webbrowser it’s not needed?