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.