Options for Lovelace on older iPad (ios 9.3.5)

Thanks Romkabouter! As you are providing useful info and don’t seem to be looking for an excuse to make me leave you alone… :wink:

I acquired 2 Ipad2s from family with the goal of displaying a dashboard for the household. Sadly, these don’t go above 9.3.5 so I can’t use any custom cards in Lovelace which works brilliant on PC and mobile.

My main purpose of the dashboard is the family calendar. I have scoured through the TileBoard files and examples and they don’t appear to support calendar displays. I have installed appDaemon and it looks like this does using a static page.

Do you have any good advice on solutions for displaying a gCal outside of Lovelace?

For Tileboard, you can use a custom tile, where customHtml can be anything you want :slight_smile:
This includes javascript and all that, I do not know how far you want and can dive into the rabbithole :wink:

This is my home page:

var CONTROLS = 
{
   title: 'Controls',
   bg: 'images/bg2.png',
   hidden: false,
   icon: 'mdi-home',
   groups: [
      {
         width: 4,
         height: 4,
         groupMarginCss: '7px 0px',
         items: [
            {
               position: [0, 0],
               width: 1,
               height: 1,
               title: 'woonkamer',
               type: TYPES.INPUT_BOOLEAN,
               state: false,
               id: 'input_boolean.avond',
               icon: 'mdi-lightbulb'
            },
            {
               position: [0, 1],
               width: 0.5,
               height: 0.5,
               classes: [CLASS_SMALL],
               title: 'bank',
               type: TYPES.LIGHT,
               id: 'light.stekker', 
               state: false,
               icon: "mdi-lightbulb"
            },
            {
               position: [0.5, 1],
               width: 0.5,
               height: 0.5,
               classes: [CLASS_SMALL],
               title: 'spot',
               type: TYPES.LIGHT,
               id: 'light.spot', 
               state: false,
               icon: "mdi-lightbulb"
            },
            {
               position: [0, 1.5],
               width: 3,
               height: 2,
               title: '',
               type: TYPES.CAMERA_THUMBNAIL,
               id: 'camera.buienradar_loop', 
               state: false,
               refresh: function () {
                  return 1000
               }
            },   
            {
               position: [0, 3.5],
               type: TYPES.CUSTOM,
               id: 'input_boolean.buienradar_regenmeter',
               title: '',
               width: 3,
               height: 1,
               state: false,
               customStyles: {'background-color': '#4682B4'},
               customHtml: function(item, entity) {
                  var d = new Date(entity.last_changed);
                  d.setSeconds(d.getSeconds() + Math.round(this.$scope.states['input_number.buienradar_regenmeter_refresh'].state));
                  if (entity.state == 'on' && Date.now() > d) {
                     //Turn off and on to update the last_changed property
                     this.api.callService('input_boolean', 'turn_off', {'entity_id' : entity.entity_id});
                     this.api.callService('input_boolean', 'turn_on', {'entity_id' : entity.entity_id});
                     //console.log(d);
                     //implemented a setTimeout, otherwise filling of the field did not work on iOS 9.3.6
                     setTimeout(function(){ 
                        var labels = new Array();
                        var values = new Array();
                        $http = angular.injector(["ng"]).get("$http");
                        $http.get("https://gpsgadget.buienradar.nl/data/raintext/?lat=53.03&lon=6.22").then(function(data) {
                        var lines = data.data.split("\n");
                           for (i=0;i<lines.length;i++) {
                              line = lines[i];
                              if (line.length > 0) {
                                 var a = line.split('|');    
                                 values.push(a[0]);
                                 labels.push(a[1]);
                              }
                           };
                           //console.log(labels);
                           if (labels.length == 0) {
                           var coeff = 1000 * 60 * 5;
                           now = new Date();
                              var rounded = new Date(Math.round(now / coeff) * coeff)
                              labels.push(rounded.getHours() + ':' + rounded.getMinutes());
                              for(i=0;i<23;i++) {
                              rounded.setMinutes(rounded.getMinutes() + 5);
                              labels.push(rounded.getHours() + ':' + rounded.getMinutes());
                              }
                           }
                           this.chart1 = new Chart('br_forecast', {
                           type: 'line',
                           data : {
                              labels: labels,
                              datasets: [
                                 {
                                    spanGaps: true,
                                    pointStyle: 'circle',
                                    data: values,
                                    borderColor: '#5F9EA0',
                                    backgroundColor: '#5F9EA0',
                                 }
                              ]
                           },
                           options: {
                              legend: {
                                 display: false
                              },
                              tooltips: {
                                 enabled:true 
                              },
                              scales: {
                                 xAxes: [{
                                    type: 'category',
                                    distribution: 'linear',
                                    time: {
                                       parser: 'h:m'
                                    },
                                    gridLines: {
                                       drawTicks: false,
                                    },
                                    ticks: {
                                       padding: 5,
                                       fontSize: 10,
                                       fontColor: '#FFF',
                                       maxRotation: 0,
                                       callback: function(value, index, values) {
                                          if (index == 0) {
                                             return "Nu";
                                          } else {
                                             if (index % 2 != 0) {return ' ';} 
                                             var date = new Date();
                                             date.setHours(value.split(':')[0]);
                                             date.setMinutes(value.split(':')[1]);                                                 
                                             //padStart does not work in iOS 9.3.6
                                             var dateString = String(date.getHours()).length < 2 ? "0" + String(date.getHours()) : String(date.getHours());
                                             dateString += ":";
                                             dateString += String(date.getMinutes()).length < 2 ? "0" + String(date.getMinutes()) : String(date.getMinutes());
                                             return dateString;
                                          }
                                       }
                                    }
                                 }],
                                 yAxes: [{
                                    gridLines: {
                                       drawTicks: false,
                                    },
                                    position: 'right',
                                    ticks: {
                                       fontSize: 10,
                                       fontColor: '#FFF',
                                       padding: 5,
                                       beginAtZero: true,
                                       suggestedMax: 255,
                                       callback: function(value, index, values) {
                                          return index == 0 ? 'Zwaar' : (index == values.length-1) ? 'Licht' : '';
                                       } 
                                    }
                                 }]
                              },
                              elements: {
                                 line: {
                                    stepped: false
                                 },
                              },
                           }
                           });
                        });
                     }, 1000);
                  }  
                  return '<div class="item-history"><div class="item-history-container"><canvas class="chart chart-line ng-isolate-scope chartjs-render-monitor" id="br_forecast"></canvas></div></div>';
               },
            },   
            {
               position: [3, 0],
               type: TYPES.WEATHER_LIST,
               width: 4,
               height: 1.5,
               title: '',
               id: {},
               state: false,
               hideHeader: false,
               primaryTitle: 'min-max',
               secondaryTitle: 'regenkans',
               dateTitle: 'dag',
               iconTitle: '',
               list: [1,2,3,4,5].map(function (id) {
                  var regen = "&sensor.br_rainchance_" + id + "d.state";
                  regen += "&sensor.br_rainchance_" + id + "d.attributes.unit_of_measurement";   

                  var temperature = "&sensor.br_minimum_temperature_" + id + "d.state - ";
                  temperature += "&sensor.br_temperature_" + id + "d.state";
                  temperature += "&sensor.br_temperature_" + id + "d.attributes.unit_of_measurement";

                  return {
                     date: function () {
                        var d = new Date(Date.now() + id * 24 * 60 * 60 * 1000);
                        //toLocaleString with localization will not work on iPad
                        var day =  d.getDay();
                        switch (day) {
                           case 0: return 'zo';
                           case 1: return 'ma';
                           case 2: return 'di';
                           case 3: return 'wo';
                           case 4: return 'do';
                           case 5: return 'vr';
                           case 6: return 'za';
                        }
                     },
                     iconImage: "&sensor.br_symbol_" + id + "d.attributes.entity_picture",
                     primary: temperature,
                     secondary: regen
                  }
               })
            },
            {
               position: [3, 1.5],
               type: TYPES.CUSTOM,
               id: 'input_boolean.buienradar_rssfeed',
               title: '',
               width: 4,
               height: 3,
               state: false,
               customStyles: {'background-color': '#4682B4'},
               customHtml: function(item, entity) {                           
                     var d = new Date(entity.last_changed);                     
                     d.setMinutes(d.getMinutes() + Math.round(this.$scope.states['input_number.buienradar_rssfeed_refresh'].state));
                     if (entity.state == 'on' && Date.now() > d) {
//                        this.api.callService('input_number', 'set_value', {'entity_id' : 'input_number.buienradar_rssfeed_refresh', 'value': Math.round(this.$scope.states['input_number.buienradar_rssfeed_refresh'].state)});
                        this.api.callService('input_boolean', 'turn_on', {'entity_id' : entity.entity_id});
                        this.$scope.states[entity.entity_id].last_changed = Date.now();
                        //implemented a setTimeout, otherwise filling of the field did not work on iOS 9.3.6
                        setTimeout(function(){ 
                           $http = angular.injector(["ng"]).get("$http");
                           $http.get("https://data.buienradar.nl/1.0/feed/xml/rssbuienradar").then(function(data) {
                              var item = new window.DOMParser()
                                    .parseFromString(data.data, "text/xml")
                                    .querySelectorAll("item")[0];
                              var d = new Date(item.querySelector("pubDate").firstChild.textContent);
                              //padStart does not work in iOS 9.3.6
                              var dateString = String(d.getDate()).length < 2 ? "0" + String(d.getDate()) : String(d.getDate());
                              dateString += "-";
                              dateString += String(d.getMonth()+1).length < 2 ? "0" + String(d.getMonth()+1) : String(d.getMonth()+1);
                              dateString += "-" + d.getFullYear();
                              var i = item.querySelector("pubDate").firstChild.textContent.split(' ');
                              i.pop();
                              i = i.slice(1);
                              document.getElementById('rssTitle').innerHTML = i.join(' ');
                              document.getElementById('rssText').innerHTML =  item.querySelector("description").firstChild.textContent;
                           });
                        }, 1000);
                     }
                  return '<div id="rssFeed"><div id="rssTitle"></div><div id="rssText"></div></div>';
               }
            },
            
         ]
      },
   ]
}

With my config like this:

function loadJS(url) {
   var xhttp = new XMLHttpRequest();
   var script = document.createElement( "script" );
   xhttp.open("GET", url, false);
   xhttp.send();
   script.text = xhttp.responseText;
   document.head.appendChild( script ).parentNode.removeChild( script );
}

loadJS('pages/cameras.js');
//loadJS('pages/weer.js');
loadJS('pages/controls.js');
//loadJS('pages/system.js');
//loadJS('pages/test.js');

var CONFIG = {
   customTheme: CUSTOM_THEMES.COMPACT, 
   transition: TRANSITIONS.ANIMATED_GPU, 
   entitySize: ENTITY_SIZES.BIG,
   tileSize: 131,
   tileMargin: 5,
   serverUrl: 'http://192.168.43.54:8123',
   wsUrl: 'ws://192.168.43.54:8123/api/websocket',
   authToken: 'longlivetokenhomeassistant',
   debug: false, 
   pingConnection: true,

   // next fields are optional
   events: [],
   timeFormat: 24,
   menuPosition: MENU_POSITIONS.LEFT, 
   hideScrollbar: true, 
   groupsAlign: GROUP_ALIGNS.HORIZONTALLY,
   onReady: function () {
      this.$scope.isPanEnabled = false;
   },

   screensaver: {
      timeout: 300, 
      slidesTimeout: 10,
      styles: { fontSize: '40px' },
      leftBottom: [
         {
            type: SCREENSAVER_ITEMS.DATETIME,
            dateFormat: 'EEEE dd LLLL',
         }
      ],
      slides: [
         {
            bg: 'images/bg2.png',
            rightTop: [ // put text to the 2nd slide
               {
                  type: SCREENSAVER_ITEMS.CUSTOM_HTML,
                  html: 'Tik om te beginnen',
                  styles: { fontSize: '40px' }
               }
            ]
         },
         {
            bg: 'images/bg2.png',
            leftTop: [ 
               {
                  type: SCREENSAVER_ITEMS.CUSTOM_HTML,
                  html: 'Tik om te beginnen',
                  styles: { fontSize: '40px' }
               }
            ]
         },
         {
            bg: 'images/bg2.png',
            rightBottom: [ 
               {
                  type: SCREENSAVER_ITEMS.CUSTOM_HTML,
                  html: 'Tik om te beginnen',
                  styles: { fontSize: '40px' }
               }
            ]
         }
      ]
   },

   header: {
      styles: {
         margin: '10px 110px 0',
         fontSize: '28px'
      },
      right: [
         {
           type: HEADER_ITEMS.WEATHER,
           styles: {
            fontSize: '20px'
           },
           fields: {
            summary: function(item,entity) {
               return this.$scope.states['sensor.br_symbol'].state;//.substring(0, 25);
            },
            temperature: '&sensor.br_temperature.state &sensor.br_temperature.attributes.unit_of_measurement (voelt als &sensor.br_feel_temperature.state)',
           }
         }
      ],
      left: [
         {
            type: HEADER_ITEMS.DATETIME,
            dateFormat: 'EEEE dd LLLL',
         }
      ]
   },

   pages: [
//      TEST,
      CONTROLS,
  //    WEER,
      CAMERAS,
 //     SYSTEM
   ],
}

It produces this page:

You cannot copy and paste this without adding some helpers to Home Assistant.
I have a input_boolean and an input_number for the weaterbox and rainprediction to refresh at a certain interval.

As you can see, the large block of text is outputting a div, which also can be scrolled.
I have a custom.css (loaded by default by Tileboard) with has some custom styles.
I wanted the light tiles to look different and I wanted rounded corners.

body, html { position: fixed; background-color: rgb(250, 250, 250); }
.-theme-compact .item {
  color: #363636;
  border-radius: 8px;
  background: #fff;
}

.item.-th-light.-on .mdi-lightbulb:before,
.item.-th-input_boolean.-on .mdi-lightbulb:before {
  color: #fdd835 !important;
}
.item.-th-light.-on, .item.-th-light.-loading,
.item.-th-input_boolean.-on, .item.-th-input_boolean.-loading {
  background-color: #fff !important;
}

#rssFeed {
  color:#fff;
  white-space:pre-wrap;
  text-align:left;
  padding:10px;
  overflow:scroll;
  max-height:360px;
}
#rssTitle {
  margin-bottom:10px;
}
.item-clickable {
  display: none !important;
}

The actual screen looks like this:

The case is this case:

But you could also go for a Vidabox, those look a lot better but are a lot more expensive as well.

Sorry for the Dutch texts :smiley:

I have check gCal and you could use a custom tile with this as customHtml:


<iframe src="https://calendar.google.com/calendar/embed?src=YOUREMAIL%40gmail.com&ctz=Europe%2FAmsterdam" style="border: 0" width="800" height="600" frameborder="0" scrolling="no"></iframe>

I have not tried it :wink:
Adjust the width and height of the tiles and page to fit your needs.

Hey There I am new to docker and the firefox docker.
The install went great on my windows 10 pc.
I started the docker with the regular example.
on the local machine i can acces port 5800 and it works great.
on my ipad mini running ios 9.3.5 or any other machine in the network i get only a link for tightVNC.
Screen Shot 2021-01-28 at 23.40.29
What am i doing wrong?

**-e “DISPLAY_WIDTH=1280” -e “DISPLAY_HEIGHT=935” ** is the best screen options for IPad2 firefox docker

2 Likes

you need to install a VNC client like RealVNC from appstore, I did it yesterday and it works!

Please tell me the way to docker lastest firefox to Ipad 2, Many thanks

Welcome to the community @td1108 ! I wasn’t sure if your question was asking about how to install Docker for Windows (since you were replying to my old post mentioning that), but if so just follow these instructions - Install Docker Desktop on Windows

By the way, I don’t use my Windows PC for home automation anymore because I needed it for other purposes. I discovered you can’t run a vpn client (for logging into a college campus) at the same time - all non-campus network connections were blocked by the vpn :frowning:

I still use my old iPads in the same way though - the iPads run a RealVNC client connected to a RaspberryPi with screensharing enabled instead (with the Pi running a full-screen browser).

Thanks for your support. I run my HASS(love lace) on an Intel NUC and I find the way to use my I pad 2 as dashboard but I can’t

Go for tileboard GitHub - resoai/TileBoard: A simple yet highly configurable Dashboard for HomeAssistant
Can be installed as add-on:
GitHub - resoai/TileBoard-addon: TileBoard add-on repository

Hello everyone. In my case with a mini ipad 2 I used a resolution of 2048 x 1536 and although it is a lot like this practically static it works perfectly. Thank you very much for the post. I had been having problems with browsers and with this post I managed to make it work.

Hi All, I would like to share my experience. I was really sad realizing that I would not be able to utilize 2x IPAD 3 that I bought for me and my wife 10 years ago. They are such a great hardware and the screen is even better than the new Galaxy Tab 8. So with some research and hints from the posts above, I would like to share how I made it (also good memory for me if I need it again in the future).
My Configuration: I have 1 Lenovo Ideapad laptop running Windows 10 as a HA Host Machine. HA is running from Oracle VM VirtualBox.

  • Install Firefox in the Host Machine (Windows 10)
  • Install Docker Desktop in the Host Machine
  • After starting the Docker and having the engine running, run the following command in your Power Shell (CMD for the old boys): jlesage/firefox - Docker Image | Docker Hub. Be aware to remove the \ symbols if you want to run it in a single command line.
  • Now install the Firefox in your Ipad 3. You should search for the purchased Apps and install the last compatible version. In case you had never installed Firefox in your Ipad, install in your Iphone (or another Apple device with the same account) and go back to the Ipad searching for the Firefox again inside the purchased apps. Ipad will ask if you want to install the last compatible version.
  • Open Firefox in your ipad and type the following Address: http://your-host-ip:5800. The IP is from your host machine (PC running Windows).
  • You will feel that there is a new Browser inside the previous Browser. This is what the docker does!
  • Now just type inside this new Browser the address to for your HA Dashboard and enjoy it!!

Thanks a lot guys! I was very beginner on this! And I am very happy to use my 2 Old IPads in each floor of the house.

2 Likes

Hi, I am planning on doing the same thing.

One question, does the iPad screen stays on all the time or you found a way to configure it to be smarter (turning on when you approach, etc)?

Thanks!

Hi @hobojoe, I didn’t get there yet. Not sure if it is even possible.
In any case my plan to use the iPad 3 is to give a function to such a great hardware, but this will not be my main tablet device for controlling my home automation. I will use it in the second floor.
If I learn how to do it I will post here.
Regards
William

That worked! Nice!

Only finding out how to set that dark theme…

1 Like

Just reading this thread, interesting indeed running a VNC… Will it also work for Android tablets ? Probably…

Is there also an addon to run a VNC server?

Did you find out how?

So I’m trying to do the same. I got firefox running inside docker and it works fine when accessing via modern browser. But on iPad mini gen 1 I’m only getting blank screen.
Tried on Firefox 8.3 (5826), Chrome 63 and whatever Safari it has - all just give blank screen :confused:

1 Like

Found solution on Reddit - per arm version of Firefox docker image is required for it to still work on iPad mini gen 1 browsers.
Below docker-compose.yml works with last available versions of Firefox and Chrome

version: '3'
services:
  firefox:
    image: jlesage/firefox:v1.18.0
    shm_size: 256M
    ports:
      - "5800:5800"
    environment:
      - DISPLAY_WIDTH=730
      - DISPLAY_HEIGHT=900
      - X11VNC_EXTRA_OPTS=-nocursor
    volumes:
      - "/home/docker/firefox/config:/config"
    restart: unless-stopped
3 Likes