30 November 2020

How I made HA-meural: a Meural Canvas integration for Home Assistant

The power to control and automate your digital art frame, just the way you like it.

By In Make 7 min read

One of my 2020 lockdown projects I’m extremely proud of is HA-meural – a custom integration for the Home Assistant home automation software that lets you control a NETGEAR Meural Canvas digital frame. It’s the result of a few months of work, lots of trial and error, and many hours lost to bad indents. But it’s also a journey that has been very fulfilling personally and has renewed my love for programming. And one that shows that building your own Home Assistant integration is much more doable than it might appear at first!

Full disclosure: As of August 1, 2023, I am an employee of Nabu Casa, the company that builds Home Assistant. This blog post was written before I started working there. My opinions on this blog are my own and do not reflect the views of Nabu Casa. Any recommendation of products here is of a personal nature and does not signal approval by Nabu Casa.

Meural Canvas digital art frames hung from the wall (from the Meural website)

Meural Canvas

This year, during the first few months of lockdown, I impulse-bought a Meural Canvas digital art frame. Photography is my main creative outlet and it’s always frustrated me that I couldn’t display more of my photos in my own home, limited by available wall-space and my inability to choose which photos to print. The Canvas can display any image I want – any of my photos I’ve taken, but also famous art from a museum or cool digital art I’ve found online. It’s amazing and my favorite purchase of the year, easy. I tweeted about my new gadget, and kinda complained that I couldn’t control the frame with Home Assistant, the system that runs my smart home. One of my smart home dreams is inspired by this scene from Antritrust, where the house knows who is present and changes the art on the walls automatically to fit their tastes.

Home Assistant integration

So what you might not know about me – once upon a time I studied computer science. Not for very long, and I ultimately switched my major to marketing, but I really enjoyed programming at the time. After graduating I did not have many opportunities to practice this skill in my daily life as a marketer, and I ended up forgetting most of what I had learned.

Home Assistant’s founder, Paulus, is a friend of mine from those student days. When he saw my tweet he replied that it was actually really simple to build your own Home Assistant integration – especially as there was a public API available! Color me intrigued. After sending him the link to Meural’s REST API documentation and giving him my login and password to test with, he had a proof of concept version of the integration up and running within half an hour. From his home in America, he could now change which playlist was displayed on the Canvas in my home in the Netherlands, all using Home Assistant. Even more impressive – he did this without owning a Canvas, or even having seen one in real life. Awesome!

And that’s when he threw the ball back to my court. The groundwork was done, now I should start getting my own hands dirty and extend the integration with all the functionality I actually wanted. That seemed a bit of a challenge, as it had been 15 years since I had last coded and Python was not a language that I was familiar with. But hey… what else are you going to do during lockdown? Might as well try my hand at this.


That was half a year ago, back in May. Now, in November, I just released HA-meural version v0.2.0 which is available to all Home Assistant users via HACS (the Home Assistant Community Store, for easy installation of unofficial add-ons), and has a whole bunch of happy users who are controlling their Canvas frames using the integration. It lets you control your Meural directly from the Home Assistant interface, which means you can easily change the displayed playlist, go to the next or previous image, pause or resume the playback and turn the Canvas on and off. The interface also shows the title, artist and year along with a thumbnail of the currently displayed artwork.

HA-meural page on Github

You can also automate the Canvas using the full capabilities that Home Assistant offers. For example, Meural already offers a simple scheduler, but it’s limited to setting up specific times for wake, sleep and playlist events. In our Home Assistant setup we use the far more powerful Node-RED visual programming language from IBM to create automations. A simple flow turns our Canvas on at 8 in the morning and off at midnight, but it also takes into account if my girlfriend and I are actually home to appreciate the art. If we’re not at home, the Canvas remains off. If we come home between 8 in the morning and midnight, the Canvas turns on. And if we leave the house, the Canvas turns off again. Pretty nice – and this is still a very simple automation, considering what Home Assistant can do.

Our time schedule automation flow in Node-RED

If you want to copy the above flow into your own Node-RED installation, you can import the following gist:

[{"id":"3d652577.d3551a","type":"bigtimer","z":"705d71ec.1ed62","outtopic":"","outpayload1":"","outpayload2":"","name":"Big Timer","comment":"","starttime":"480","endtime":"0","starttime2":0,"endtime2":0,"startoff":0,"endoff":0,"startoff2":0,"endoff2":0,"offs":0,"outtext1":"on","outtext2":"off","timeout":1440,"sun":true,"mon":true,"tue":true,"wed":true,"thu":true,"fri":true,"sat":true,"jan":true,"feb":true,"mar":true,"apr":true,"may":true,"jun":true,"jul":true,"aug":true,"sep":true,"oct":true,"nov":true,"dec":true,"day1":0,"month1":0,"day2":0,"month2":0,"day3":0,"month3":0,"day4":0,"month4":0,"day5":0,"month5":0,"day6":0,"month6":0,"day7":"","month7":"","day8":"","month8":"","day9":"","month9":"","day10":"","month10":"","day11":"","month11":"","day12":"","month12":"","d1":0,"w1":0,"d2":0,"w2":0,"d3":0,"w3":0,"d4":0,"w4":0,"d5":0,"w5":0,"d6":0,"w6":0,"xday1":0,"xmonth1":0,"xday2":0,"xmonth2":0,"xday3":0,"xmonth3":0,"xday4":0,"xmonth4":0,"xday5":0,"xmonth5":0,"xday6":0,"xmonth6":0,"xd1":0,"xw1":0,"xd2":0,"xw2":0,"xd3":0,"xw3":0,"xd4":0,"xw4":0,"xd5":0,"xw5":0,"xd6":0,"xw6":0,"suspend":false,"random":false,"repeat":true,"atstart":true,"odd":false,"even":false,"x":140,"y":280,"wires":[[],[],["1f13e5ed.0eb8fa"]]},{"id":"1f13e5ed.0eb8fa","type":"switch","z":"705d71ec.1ed62","name":"","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"on","vt":"str"},{"t":"eq","v":"off","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":270,"y":280,"wires":[["663e4a3e.243654"],["8d1e7911.aa2358"]]},{"id":"663e4a3e.243654","type":"api-current-state","z":"705d71ec.1ed62","name":"Is Guy home?","version":1,"outputs":2,"halt_if":"home","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"person.guy","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":480,"y":200,"wires":[["c7dc84bd.e670a8"],["179c8348.50a85d"]]},{"id":"179c8348.50a85d","type":"api-current-state","z":"705d71ec.1ed62","name":"Is Marrit home?","version":1,"outputs":2,"halt_if":"home","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"person.marrit","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":480,"y":260,"wires":[["c7dc84bd.e670a8"],["8d1e7911.aa2358"]]},{"id":"da48b098.1f904","type":"api-call-service","z":"705d71ec.1ed62","name":"Turn on Meural Canvas","version":1,"debugenabled":false,"service_domain":"media_player","service":"turn_on","entityId":"media_player.mirror","data":"{}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1230,"y":220,"wires":[[]]},{"id":"27863543.c6435a","type":"api-call-service","z":"705d71ec.1ed62","name":"Turn off Meural Canvas","version":1,"debugenabled":false,"service_domain":"media_player","service":"turn_off","entityId":"media_player.mirror","data":"{}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":1230,"y":320,"wires":[[]]},{"id":"f8836f1f.0018a","type":"server-state-changed","z":"705d71ec.1ed62","name":"If Guy leaves home","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"person.guy","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"home","halt_if_type":"str","halt_if_compare":"is_not","outputs":2,"output_only_on_state_change":true,"x":170,"y":380,"wires":[["864c1479.e3bd28"],[]]},{"id":"864c1479.e3bd28","type":"api-current-state","z":"705d71ec.1ed62","name":"and Marrit is not home","version":1,"outputs":2,"halt_if":"home","halt_if_type":"str","halt_if_compare":"is_not","override_topic":false,"entity_id":"person.marrit","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":460,"y":380,"wires":[["8d1e7911.aa2358"],[]]},{"id":"937d1310.f2c74","type":"server-state-changed","z":"705d71ec.1ed62","name":"If Marrit leaves home","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"person.marrit","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"home","halt_if_type":"str","halt_if_compare":"is_not","outputs":2,"output_only_on_state_change":true,"x":180,"y":440,"wires":[["354472f1.4f79ae"],[]]},{"id":"354472f1.4f79ae","type":"api-current-state","z":"705d71ec.1ed62","name":"and Guy is not home","version":1,"outputs":2,"halt_if":"home","halt_if_type":"str","halt_if_compare":"is_not","override_topic":false,"entity_id":"person.guy","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":460,"y":440,"wires":[["8d1e7911.aa2358"],[]]},{"id":"c58e5e47.1db2c","type":"server-state-changed","z":"705d71ec.1ed62","name":"If Guy comes home","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"person.guy","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"home","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"x":170,"y":100,"wires":[["f756a0f.ffa1d6"],[]]},{"id":"3968b4c9.84d13c","type":"server-state-changed","z":"705d71ec.1ed62","name":"If Marrit comes home","version":1,"exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"entityidfilter":"person.marrit","entityidfiltertype":"exact","outputinitially":false,"state_type":"str","haltifstate":"home","halt_if_type":"str","halt_if_compare":"is","outputs":2,"output_only_on_state_change":true,"x":180,"y":160,"wires":[["f756a0f.ffa1d6"],[]]},{"id":"f756a0f.ffa1d6","type":"time-range-switch","z":"705d71ec.1ed62","name":"Between 08:00 and 00:00","startTime":"08:00","endTime":"00:00","startOffset":0,"endOffset":0,"x":450,"y":100,"wires":[["c7dc84bd.e670a8"],[]]},{"id":"8d1e7911.aa2358","type":"api-current-state","z":"705d71ec.1ed62","name":"Is Meural Canvas on?","version":1,"outputs":2,"halt_if":"off","halt_if_type":"str","halt_if_compare":"is_not","override_topic":false,"entity_id":"media_player.mirror","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":740,"y":320,"wires":[["27863543.c6435a"],[]]},{"id":"c7dc84bd.e670a8","type":"api-current-state","z":"705d71ec.1ed62","name":"Is Meural Canvas off?","version":1,"outputs":2,"halt_if":"off","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"media_player.mirror","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":740,"y":220,"wires":[["b38a07c7.73db58"],[]]},{"id":"b38a07c7.73db58","type":"api-current-state","z":"705d71ec.1ed62","name":"Is Vacation mode on?","version":1,"outputs":2,"halt_if":"on","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"input_boolean.vacation_mode","state_type":"str","state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","blockInputOverrides":false,"x":980,"y":220,"wires":[[],["da48b098.1f904"]]}]

Integrating the Canvas into Home Assistant has also unlocked a functionality that is often requested by Meural users. The Canvas only supports Amazon Alexa out-of-the-box, so you’re shit out of luck if you have a Google Home. But as Home Assistant can expose devices to Google for voice control, you can control your Meural Canvas using Google Home through HA-meural. An easy fix for something that has been frustrating Canvas owners all over the world.

My original proof-of-concept Google Home video

Learning to code – again

Making this has been a shitload of fun. I had forgotten just how enjoyable it is to program something and then see it affect the world around me. For the past few months I’ve been spending my evenings, here and there, just messing with the integration. It’s awesome to spend a few hours figuring out these little logical puzzles in the code and then get rewarded by seeing the Canvas act just a little bit smarter. And it’s been worthwhile to pick this skill up outside of this project too – much has changed over the years and some ability to code in Python has become a valuable skill inside marketing too.

HA-meural in the Home Assistant iOS app

And the crazy thing is – it wasn’t nearly as hard as I expected it to be. I do realize I had some unfair advantages – the first version of the code wasn’t written by me, after all, I extended an integration that could already log into the Meural server and retrieve the basic information required to operate. And I do have some prior programming experience, even if I don’t remember much from my time as a student. Still, having gone through a few months of figuring things out, I’m convinced that this is not as hard as it looks from the outside.

After all, you’re not starting from scratch. There’s official Home Assistant developer documentation. The code of every other add-on, official and unofficial, is out there on Github for you to learn from. There’s tons of general Python tutorials and guides online. There’s Stack Overflow if you need help with a specific problem. And of course there are the Home Assistant forums. I started discussing the integration there and mentioned that I was having trouble coding the upload of images to the Canvas. A key functionality we needed to really extend the use of the Canvas for smart homes. Within days @thomasvs contributed the code to upload an image to the Canvas using the preview functionality, allowing us to display any arbitrary image on the device!

Python is not a language I’m very familiar with. I don’t have the experience of proper code design patterns an experienced dev would have. So all in all, HA-meural‘s code probably reads like a freshman ‘intro to programming’ homework assignment. But at the end of the day – it works. Various iterations of this code have been running in my home for the past half year now and it’s been good at doing what it’s supposed to do. And when you get down to it, that’s good enough for me. If that’s good enough for you, believe me: if I can do this, so can you.

What do you think?