The advantage of making something yourself rather than just going out and buying it off the shelf is that you can tailor everything to your exact requirements and ideas.
So when I set out to build a digital picture frame, I also wanted to integrate it tightly with my home automation system, “Home Assistant“.
The idea was to control the picture frame from within Home Assistant fully. This would include choosing the playback directory, seeing all the relevant Exif information, a map with the location where the photo was taken, and even showing the photo itself on display on the picture frame.
Paddy’s old PictureFrame script originated from a simple Pi3D demo file, and a lot of great features had been added over time. But its code also became very complex and needed a spring cleaning.
So, together with Paddy and Jeff, we completely rewrote the old Pi3D PictureFrame script and added lots of new features in the process. Amongst them was a perfect integration with Home Assistant!
If you are not yet familiar with PictureFrame, head over here first.
Tested with: Raspberry Pi OS March 2021 version, Raspberry Pi 2, 3, and 4, Pi3D 2.43, PictureFrame 2021.03.20, Home Assistant 2021.3.4, 1080p and 4K displays.
Benefits of a tight integration between Home Assistant and PictureFrame
The PictureFrame 2021 image viewer allows you to remote control many features and retrieve Exif data from the images. The new version now even has a web server that allows you to remotely view the currently displayed image.
The whole control panel in Home Assistant will look like this:

You can select the right controls and data fields that are of interest to you and create your own customized picture frame dash board.
Installation
The following assumes that you have successfully completed the PictureFrame basic installation and that you have Home Assistant running.
If you don’t use an external MQTT broker, you can install one on your Raspberry Pi by entering
sudo apt install -y mosquitto mosquitto-clients
Configuration of PictureFrame
The configuration of PictureFrame is made in configuration.yaml
.
In the mqtt
section enable MQTT and enter your IP and port.
mqtt:
use_mqtt: True # Set True, to enable mqtt
server: 192.168.178.136 # Enter the IP where your MQTT is hosted
port: 1883 # this works for most people
login: "" # your mqtt user. If you have none, just leave empty
password: "" # password for mqtt user. If you have none, just leave empty
tls: "" # this is for secure connections. If you don't know what this is, leave it empty
device_id: "picframe" # unique id of device. Change if there is more than one PictureFrame
If you use the above settings, you only have to insert your IP and you can leave the rest as it is.
Directly below in the http
section, enable it. If you are not using an SSL connection, the settings below should work for you.
http:
use_http: True # default=False. Set True to enable http
path: "/home/pi/picframe_data/html" # path to where html files are located
port: 9000 # port used to serve pages by http server < 1024 requires root which is *bad* idea
use_ssl: False
keyfile: "" # private-key
certfile: "" # server certificate
Start PictureFrame and keep it running while we configure Home Assistant.
To check if MQTT works alright, you can use a tool like MQTT Explorer to see if the sensors and switches data are being transmitted.
If what you see looks similar to the above picture, you’re off to a good start.
Home Assistant uses “Sensors” and “Switches”. A switch can just be “ON” or “OFF” whereas a sensor can have specific values like dates, numbers, or strings.
Configuration of Home Assistant
If you haven’t worked with MQTT before, you need to add the MQTT Integration. In most cases, you only need to enter the IP of your MQTT server. The port is 1883 for most users.
The Options, “Auto Discovery” needs to be on, the rest you can ignore for the time being.
Click “Submit” and click on “devices” which will take you to the the “Devices” section. You should see “picframe”. Click on it.
You can already see all the entities that are being sent from PictureFrame to Home Assistant.
Play around with some of the switches like e.g. “picframe_next” to move the PictureFrame to the next image to see if it works.
Once this works, we will configure several settings which allow us to fully control PictureFrame from within Home Assistant.
Installing the Helpers
Switches and sensors can be used directly in Home Assistant cards. For input elements like text fields or the brightness slider, you have to create the elements first. This is done with “helpers”. You can find them in the configuration section.
Unfortunately, Home Assistant doesn’t provide a YAML import for helpers. You have to create them over the GUI manually.
I will show you screenshots of each Helper so that you know exactly where you need to enter what information. Just click on “Add Helper”.
The Entity ID will be added automatically once you save the Helper.
Let’s start with the first one.
Helper picframe.time_delay
Add a Helper “Number”.
Helper picframe.fade_time
Add a Helper “Number”.
Helper picframe.brightness
Add a Helper “Number”.
Helper picframe_date_from
To pick a date, we must first define the local data format, which can be processed. You could use the date picker helper for date input, but it is limited to standard Unix time. While this is probably fine for most users, some may want to include scanned images from much earlier.
While this is possible, it is a bit more complicated because it requires a regular expression pattern (“regex”) for client-side valuation.
A regex pattern is a sequence of characters that specifies a search pattern. Such patterns are used by string-searching algorithms, or for input validation. So exactly what we need for specifying a date range.
Here are a few examples:
Format | Regex |
dd.mm.yy | ^(0[1-9]|[12][0-9]|3[01])\.(0[1-9]|1[012])\.\d{4}$ |
YYYY-mm-dd | ^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$ |
mm/dd/YYYY | ^(0[1-9]|1[012])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$ |
dd/mm/YYYY | ^(0[1-9]|[12][0-9]|3[01])\/(0[1-9]|1[012])\/\d{4}$ |
Add a Helper “Text”.
Helper picframe_date_to
Add a Helper “Text”.
Helper picframe_location_filter
Add a Helper “Text”.
Helper picframe_tags_filter
Add a Helper “Text”.
Helper picframe_dir
Add a Helper “Dropdown”.
Under “Options” just add one dummy directory. It will later be overwritten with the actual content of your Pictures folder.
So, all in all your Helper selection should include all these:
Creating the Cards
Now go back to your Lovelace interface and add another view. You can always move the card later but for the moment, let’s keep it a separate view.
Images Card
This card shows you all the image metadata
Create a new card, pick “Entities”. You will see a few pre-filled entities which we are not going to use. Instead, click on “Show Code Editor”.
Erase the sample text in there and replace it with:
cards:
- type: markdown
content: >-
{% if state_attr("sensor.picframe_image", "IPTC Object Name") -%}
## {{ state_attr("sensor.picframe_image", "IPTC Object Name") }}
{%- else -%}
## {{ states("sensor.picframe_image") }}
{%- endif %}
{% if state_attr("sensor.picframe_image", "IPTC Caption/Abstract") -%}
{{ state_attr("sensor.picframe_image", "IPTC Caption/Abstract") }}
{% endif %}
 | urlencode }})
{% if state_attr("sensor.picframe_image", "location") != None %}
{{ state_attr("sensor.picframe_image", "location") }}
{%- else -%}
No GPS data.
{%- endif %}
- type: glance
entities:
- entity: switch.picframe_back
tap_action:
action: toggle
- entity: switch.picframe_paused
tap_action:
action: toggle
- entity: switch.picframe_next
tap_action:
action: toggle
- entity: switch.picframe_shuffle
tap_action:
action: toggle
- entity: switch.picframe_delete
tap_action:
action: toggle
show_name: false
show_state: false
state_color: true
show_icon: true
- type: entities
entities:
- entity: sensor.picframe_image
name: File
- entity: sensor.picframe_image_date
icon: 'mdi:calendar-clock'
name: Date
- type: attribute
entity: sensor.picframe_image
attribute: EXIF ExposureTime
unit: sec
icon: 'mdi:camera-timer'
name: Exposure
- type: attribute
entity: sensor.picframe_image
attribute: EXIF FNumber
unit: f
icon: 'mdi:camera-iris'
name: Aperture
- type: attribute
entity: sensor.picframe_image
attribute: EXIF ISOSpeedRatings
icon: 'mdi:film'
name: ISO
- type: attribute
entity: sensor.picframe_image
attribute: EXIF FocalLength
unit: iso
icon: 'mdi:signal-distance-variant'
name: Focal Length
- type: attribute
entity: sensor.picframe_image
attribute: Image Model
unit: mm
icon: 'mdi:camera'
name: Camera Model
- type: attribute
entity: sensor.picframe_image
attribute: IPTC Keywords
icon: 'mdi:tag'
name: Keywords
- aspect_ratio: '3:2'
dark_mode: false
default_zoom: 11
entities:
- entity: sensor.picframe_image
type: map
hours_to_show: 0
type: vertical-stack
Search for “
- entity: input_text.picframe_date_to
name: Filter on date up to (DD.MM.YYYY)
show_header_toggle: false
theme: Backend-selected
title: Filter
type: entities
state_color: true
Text Overlay Card
This card allows you to control the image information.
Create yet another card and paste this into the YAML editor.
type: entities
entities:
- entity: switch.picframe_title_toggle
name: Title
- entity: switch.picframe_caption_toggle
name: Caption
- entity: switch.picframe_name_toggle
name: File Name
- entity: switch.picframe_location_toggle
name: Location
- entity: switch.picframe_date_toggle
name: Capture Date
- entity: switch.picframe_directory_toggle
name: Directory
- entity: switch.picframe_text_refresh
name: Text refresh
title: Text-Overlay
state_color: true
Settings Card
This card controls the screen brightness, display on/off settings, and the image transition.
entities:
- entity: switch.picframe_display
name: Living Room Frame
- entity: input_number.picframe_brightness
name: Brightness
- entity: input_number.picframe_time_delay
name: Duration
- entity: input_number.picframe_fade_time
name: Fade Time
show_header_toggle: false
theme: Backend-selected
title: Digital Picture Frame
type: entities
state_color: true
Automations
The automation settings perform several useful tasks like changing the date format you are expecting in date_from / date_to helpers and managing the input variables overall.
The time format can be found here: docs.python.org datetime.
Instead of creating seven separate automations, you can append the code below to the automation.yaml file. The easiest way is to use the file editor in Home Assistant. Save and restart the Home Assistant Server once you are done.
- id: pic1607067277342
alias: picframe_directory_get
description: ''
trigger:
- platform: state
entity_id: sensor.picframe_directory
- platform: homeassistant
event: start
condition:
- condition: or
conditions:
- condition: template
value_template: '{{ ( states("sensor.picframe_directory") ) != ( states("input_select.picframe_directory")
) }}'
- condition: template
value_template: '{{ ( state_attr("sensor.picframe_directory", "directories")
) != ( state_attr("input_select.picframe_directory", "options") ) }}'
action:
- service: input_select.set_options
data:
options: '{{ state_attr(''sensor.picframe_directory'', ''directories'') }}'
entity_id: input_select.picframe_directory
- service: input_select.select_option
data:
option: '{{ states("sensor.picframe_directory") }}'
entity_id: input_select.picframe_directory
mode: single
- id: pic1607067504666
alias: picframe_directory_set
description: ''
trigger:
- platform: state
entity_id: input_select.picframe_directory
condition: []
action:
- service: mqtt.publish
data:
topic: picframe/directory
payload: '{{ states("input_select.picframe_directory") }}'
mode: single
- id: pic1607456756183
alias: picframe_fade_time_get
description: ''
trigger:
- platform: state
entity_id: sensor.picframe_fade_time
condition:
- condition: template
value_template: '{{ states("input_number.picframe_fade_time") != states("sensor.picframe_fade_time")
}}'
action:
- service: input_number.set_value
data:
value: '{{ states("sensor.picframe_fade_time") }}'
entity_id: input_number.picframe_fade_time
mode: single
- id: pic1607456736910
alias: picframe_time_delay_get
description: ''
trigger:
- platform: state
entity_id: sensor.picframe_time_delay
condition:
- condition: template
value_template: '{{ states("input_number.picframe_time_delay") != states("sensor.picframe_time_delay")
}}'
action:
- service: input_number.set_value
data:
value: '{{ states("sensor.picframe_time_delay") }}'
entity_id: input_number.picframe_time_delay
mode: single
- id: pic1607456731052
alias: picframe.date_from_get
description: ''
trigger:
- platform: state
entity_id: sensor.picframe_date_from
condition:
- condition: template
value_template: '{{ as_timestamp(strptime(states("input_text.picframe_date_from"),"%d.%m.%Y"))
| int != states("sensor.picframe_date_from")|int }}'
action:
- service: input_text.set_value
data:
value: '{{ states(''sensor.picframe_date_from'') | int | timestamp_custom(''%d.%m.%Y'')
}}'
entity_id: input_text.picframe_date_from
mode: single
- id: pic1607456741584
alias: picframe.date_to_get
description: ''
trigger:
- platform: state
entity_id: sensor.picframe_date_to
condition:
- condition: template
value_template: '{{ as_timestamp(strptime(states("input_text.picframe_date_to"),"%d.%m.%Y"))
| int != states("sensor.picframe_date_to")|int }}'
action:
- service: input_text.set_value
data:
value: '{{ states(''sensor.picframe_date_to'') | int | timestamp_custom(''%d.%m.%Y'')
}}'
entity_id: input_text.picframe_date_to
mode: single
- id: pic1607456720004
alias: picframe_fade_time_set
description: ''
trigger:
- platform: state
entity_id: input_number.picframe_fade_time
condition:
- condition: template
value_template: '{{ states("input_number.picframe_fade_time") != states("sensor.picframe_fade_time")
}}'
action:
- service: mqtt.publish
data:
topic: picframe/fade_time
payload: '{{ states("input_number.picframe_fade_time") }}'
mode: single
- id: pic1607467027848
alias: picframe_time_delay_set
description: ''
trigger:
- platform: state
entity_id: input_number.picframe_time_delay
condition:
- condition: template
value_template: '{{ states("input_number.picframe_time_delay") != states("sensor.picframe_time_delay")
}}'
action:
- service: mqtt.publish
data:
topic: picframe/time_delay
payload: '{{ states("input_number.picframe_time_delay") }}'
mode: single
- id: pic1607457675153
alias: picframe.date_to_set
description: ''
trigger:
- platform: state
entity_id: input_text.picframe_date_to
condition:
- condition: template
value_template: '{{ as_timestamp(strptime(states("input_text.picframe_date_to"),"%d.%m.%Y"))
| int != states("sensor.picframe_date_to")|int }}'
action:
- service: mqtt.publish
data:
topic: picframe/date_to
payload: "{% if states(\"input_text.picframe_date_to\") != \"\" -%} \n\
\ {{ as_timestamp(strptime(states(\"input_text.picframe_date_to\"),\"\
%d.%m.%Y\")) | int }} \n{%- endif %}"
- condition: template
value_template: '{{ states("input_text.picframe_date_to") == "" }}'
- service: input_text.set_value
data:
value: '{{ states(''sensor.picframe_date_to'') | int | timestamp_custom(''%d.%m.%Y'')
}}'
entity_id: input_text.picframe_date_to
mode: single
- id: pic1607466674037
alias: picframe.date_from_set
description: ''
trigger:
- platform: state
entity_id: input_text.picframe_date_from
condition:
- condition: template
value_template: '{{ as_timestamp(strptime(states("input_text.picframe_date_from"),"%d.%m.%Y"))
| int != states("sensor.picframe_date_from")|int }}'
action:
- service: mqtt.publish
data:
topic: picframe/date_from
payload: "{% if states(\"input_text.picframe_date_from\") != \"\" -%} \n\
\ {{ as_timestamp(strptime(states(\"input_text.picframe_date_from\"),\"\
%d.%m.%Y\")) | int }} \n{%- endif %}"
- condition: template
value_template: '{{ states("input_text.picframe_date_from") == "" }}'
- service: input_text.set_value
data:
value: '{{ states(''sensor.picframe_date_from'') | int | timestamp_custom(''%d.%m.%Y'')
}}'
entity_id: input_text.picframe_date_from
mode: single
- id: pic1613336726633
alias: picframe_brightness_get
description: ''
trigger:
- platform: state
entity_id: sensor.picframe_brightness
condition:
- condition: template
value_template: '{{ states("input_number.picframe_brightness") | int != states("sensor.picframe_brightness")
| int * 100 }}'
action:
- service: input_number.set_value
data:
value: '{{ states("sensor.picframe_brightness") | int * 100 }}'
entity_id: input_number.picframe_brightness
mode: single
- id: pic1613367010761
alias: picframe_brightness_set
description: ''
trigger:
- platform: state
entity_id: input_number.picframe_brightness
condition:
- condition: template
value_template: '{{ states("input_number.picframe_brightness") | int != states("sensor.picframe_brightness")
| int * 100 }}'
action:
- service: mqtt.publish
data:
topic: picframe/brightness
payload: '{{ states("input_number.picframe_brightness") | float / 100.0 }}'
mode: single
- id: pic1613395679608
alias: picframe_location_filter_set
description: ''
trigger:
- platform: state
entity_id: input_text.picframe_location_filter
condition: []
action:
- service: mqtt.publish
data:
topic: picframe/location_filter
payload: '{{ states("input_text.picframe_location_filter") }}'
mode: single
- id: pic1616758160875
alias: picframe_tags_filter_set
description: ''
trigger:
- platform: state
entity_id: input_text.picframe_tags_filter
condition: []
action:
- service: mqtt.publish
data:
topic: picframe/tags_filter
payload: '{{ states("input_text.picframe_tags_filter") }}'
mode: single
- id: pic1613567985293
alias: picframe_location_filter_get
description: ''
trigger:
- platform: state
entity_id: sensor.picframe_location_filter
condition:
- condition: template
value_template: "{% if is_state('sensor.picframe_location_filter', 'None')\
\ %}\n {{ states('input_text.picframe_location_filter') != '' }}\n{%\
\ else %}\n {{ states('input_text.picframe_location_filter') != states('sensor.picframe_location_filter')\
\ }}.\n{% endif %}"
action:
- service: input_text.set_value
data:
value: '{{ states("sensor.picframe_location_filter") }}'
entity_id: input_text.picframe_location_filter
mode: single
- id: pic1613673115253
alias: picframe_location_tags_get
description: ''
trigger:
- platform: state
entity_id: sensor.picframe_tags_filter
condition:
- condition: template
value_template: "{% if is_state('sensor.picframe_tags_filter', 'None') %}\n\
\ {{ states('input_text.picframe_tags_filter') != '' }}\n{% else %}\n\
\ {{ states('input_text.picframe_tags_filter') != states('sensor.picframe_tags_filter')\
\ }}.\n{% endif %}"
action:
- service: input_text.set_value
data:
value: '{{ states("sensor.picframe_tags_filter") }}'
entity_id: input_text.picframe_tags_filter
mode: single
Home Assistant Configuration.yaml
This refers to the configuration.yaml
of Home Assistant not the file of the same name in PictureFrame.
Open configuration.yaml
and add the following paragraph.
sensor:
- platform: template
sensors:
picframe_image_date:
value_template: "{{ state_attr('sensor.picframe_image', 'EXIF DateTimeOriginal') | int | timestamp_custom('%d. %b. %Y %H:%M') }}"
friendly_name: "Date"
The time format could be found here: docs.python.org datetime
Finally, restart Home Assistant.
Go to the Automations again and trigger the “picframe_directory_get” automation manually once by clicking “Execute”.
Go to your Home Assistant dashboard. As soon as the next image change happens on PictureFrame, the data will be shown.
How to integrate more than one picture frame
Setting up two or more picture frames is very simple.
In the configuration.yaml of your other picture frame, give it a different name than the default “picframe”.
Then repeat all the above steps but replace every instance of “picframe” with your new name. This means, helper, cards, and automations. It is easily done with Find&Replace with an editor like Sublime Text.
Note that you will have to change the ID of the every duplicate automation to something different. The easiest is to just change “pic” to “pictv” or whatever you have in mind. But it can be any string as long as it is unique.
- id: 'pic1613673115253'
If you don’t, the automations will be ignored by Home Assistant. And don’t forget to add a second sensor in the Home Assistant configuration.yaml file and to restart.
Troubleshooting
If everything works, but you can’t see the image, make sure that you are either using HTTP or HTTPS on both Home Assistant and in the settings in PictureFrame.
The image will only work in your local network. If you are using e.g. Nabu Casa to access your Home Assistant instance, you won’t be able to see the image.
Also, it may take “one round” of images for the database to have all the information on keywords and locations.
If you can’t enter keywords or locations, click on the icon and enter your value there. You only need to do it once, it will be fine afterwards.
Conclusion
Integrating PictureFrame into Home Assistant allows to use all the great smart list features and to see the metadata extracted from the images. It really makes the software a lot easier to use overall.
I admit that it’s a bit of work, but it’s not hard, and with the detailed instructions in this article, it should be easy for you to achieve this high integration level between PictureFrame 2021 and Home Assistant.
Enjoy!
Related Articles
- How to trigger a Home Assistant script through Alexa and make your Raspberry Pi picture frame do (just about) anything
- How to pause, go back or delete an image on your Raspberry Pi digital picture frame with a simple Alexa voice command
- How to automatically control the display brightness of your Raspberry Pi photo frame
- Activate the power of the magic matting feature on your Raspberry Pi picture frame