TileBoard - New dashboard for Homeassistant

Hello! Is it possible to use ng-binds inside of ‘customHtml’ property?
Is it possible to create PopUp with customHtml?
And also, is it possible to make secondaryAction to open PopUp with customHtml?

Answering to myself.
Yes, it’s possible.
using ‘bind-html-compile’ from GitHub - incuna/angular-bind-html-compile: Directive that calls $compile on trusted HTML, allowing directives in an API response.
instead of ‘ng-bind-html’ in the TYPES.CUSTOM tile.
Here is weather forecast group made lovelace look-a-like by TYPES.CUSTOM tile with customHTML:

I only need to make it non-clickable.

1 Like

Looks nice. Can you give an example of your code? I like to learn from it.

Actually it is not my code. I’ve ported it from TYPES.WEATHER tiles and https://github.com/bramkragten/weather-card Lovelace weather card.

Here is diff for make ‘bind-html-compile’ working, and part of my config.js

diff --git a/index.html b/index.html
index 11ca2d8..927828e 100644
--- a/index.html
+++ b/index.html
@@ -36,6 +36,7 @@
          'styles/weather-icons.css',
          'styles/color-picker.min.css',
          'styles/custom.css',
+         'styles/materialdesignicons.min.css',
       ].map(function (value) {
          document.write('<li'+'nk rel="stylesheet" href="'+value+suffix+'"/>');
       });
@@ -50,6 +51,8 @@
          'scripts/vendors/Chart.min.js',  // Required for history charts
          'scripts/vendors/angular-chart.min.js',  // Required for linking charts to angular.js
          'scripts/vendors/hls.js',
+         'scripts/vendors/angular-locale_ru-ru.min.js',
+         'scripts/vendors/angular-bind-html-compile.min.js',
 
          'scripts/models/noty.js',
          'scripts/app.js',
@@ -641,7 +644,7 @@
            class="item-entity-container">
 
          <div ng-if="item.customHtml"
-              ng-bind-html="itemCustomHtml(item, entity)"></div>
+              bind-html-compile="itemCustomHtml(item, entity)"></div>
 
          <div ng-if="!item.customHtml" class="item-entity">
             <span class="item-entity--icon mdi"
@@ -1250,9 +1253,10 @@
 </script>
 
 
-
+<!--
 <link rel="stylesheet" async
       href="https://cdn.materialdesignicons.com/3.8.95/css/materialdesignicons.min.css">
+-->
 
 </body>
 </html>
diff --git a/scripts/init.js b/scripts/init.js
index 1ea7d32..f28c3a9 100644
--- a/scripts/init.js
+++ b/scripts/init.js
@@ -5,7 +5,7 @@ if(!window.CONFIG) {
    alert(error);
 }
 
-var App = angular.module('App', ['hmTouchEvents', 'colorpicker', 'angularjs-gauge', 'chart.js']);
+var App = angular.module('App', ['hmTouchEvents', 'colorpicker', 'angularjs-gauge', 'chart.js', 'angular-bind-html-compile']);
 
 App.config(function($sceProvider, $locationProvider, ApiProvider, ChartJsProvider) {
    $sceProvider.enabled(false);

const windDirections = [
  "N", "NNE", "NE", "ENE",
  "E", "ESE", "SE", "SSE",
  "S", "SSW", "SW", "WSW",
  "W", "WNW",  "NW","NNW",
  "N"
];

const windDirections_ru = [
  "С", "ССВ", "СВ", "ВСВ",
  "В", "ВЮВ", "ЮВ", "ЮЮВ",
  "Ю", "ЮЮЗ", "ЮЗ", "ЗЮЗ",
  "З", "ЗСЗ",  "СЗ","ССЗ",
  "С"
];

const weather_icons = {
	'sunny': 'clear',
        'clear-night': 'clear',
        'cloudy': 'cloudy',
        'fog': 'fog',
        'hail': 'sleet',
        'lightning': 'tstorms',
	'lightning-rainy': 'tstorms',
	'pouring': 'rain',
        'rainy': 'rain',
        'snowy-rainy': 'sleet',
        'snowy': 'snow',
        'windy': 'hazy',
        'windy-variant': 'hazy',
        'partlycloudy': 'partlycloudy'
};


var weather_current = {
//    type: TYPES.WEATHER_LOVELACE,
    type: TYPES.CUSTOM,
    id: {}, // using empty object for an unknown id
    icon: function( ){
	var icon = this.states['sensor.gismeteo_condition'].state;
        var map = weather_icons;
        var sun = this.states['sun.sun'].state;
        if (sun && sun == 'below_horizon'){
          if (map && map[icon]) return 'nt-'+map[icon];
    	  return 'nt-'+icon;
        } else {
          return map[icon] || icon;
        }
      },
    fields: { // most of that fields are optional
        summary: '&sensor.gismeteo_condition.state',
        temperature: '&sensor.gismeteo_temperature.state',
        temperatureUnit: '&sensor.gismeteo_temperature.attributes.unit_of_measurement',
        windSpeed: function() {
    	    return parseFloat(this.states['sensor.gismeteo_wind_speed'].state).toFixed(0);
        },
        windSpeedUnit: '&sensor.gismeteo_wind_speed.attributes.unit_of_measurement',
        humidity: '&sensor.gismeteo_humidity.state',
        humidityUnit: '&sensor.gismeteo_humidity.attributes.unit_of_measurement',
        pressure: '&sensor.gismeteo_pressure.state',
        pressureUnit: '&sensor.gismeteo_pressure.attributes.unit_of_measurement',
        sun: '&sun.sun.state',
        sunset: function() {
    	    return new Date(this.states['sun.sun'].attributes.next_setting).toLocaleTimeString('en', {
                        hour12: false, hour: '2-digit', minute: '2-digit' });
        },
        sunrise: function() {
    	    return new Date(this.states['sun.sun'].attributes.next_rising).toLocaleTimeString('en', {
                        hour12: false, hour: '2-digit', minute: '2-digit' });
        },
        windBearing: function() {
    	    return windDirections_ru[parseInt((this.states['weather.gismeteo'].attributes.wind_bearing + 11.25) / 22.5)];
        },
    },
    customHtml: `
         <div class="weather -lovelace">
            <div class="weather-icon -lovelace"	ng-if="(_icon = getWeatherIcon(item, entity))">
            	<div class="wu " ng-class="'wu-' + _icon"></div>
            </div>
            <div class="weather-temperature -lovelace" ng-if="item.fields.temperature">
               <span ng-bind="getWeatherField('temperature', item, entity)"></span>
               <span class="weather-temperature-unit -lovelace" ng-bind="getWeatherField('temperatureUnit', item, entity)"></span>
            </div>
    	    <div class="weather-line -items -lovelace">
	    <ul class="variations">
            <li>
               <span ng-if="item.fields.humidity" class="weather-item -lovelace">
                  <i class="mdi mdi-water"></i>
                  <span ng-bind="getWeatherField('humidity', item, entity)"></span>
                  <span ng-bind="getWeatherField('humidityUnit', item, entity)"></span>
               </span>
            </li>
            <li>
               <span ng-if="item.fields.windSpeed" class="weather-item -lovelace">
                  <i class="mdi mdi-weather-windy"></i>
                  <span ng-bind="getWeatherField('windSpeed', item, entity)"></span>
                  <span ng-bind="getWeatherField('windSpeedUnit', item, entity)"></span>
               </span>
            </li>
            <li>
               <span class="weather-item -lovelace" ng-if="item.fields.pressure">
                  <i class="mdi mdi-gauge"></i>
                  <span ng-bind="getWeatherField('pressure', item, entity)"></span>
                  <span ng-bind="getWeatherField('pressureUnit', item, entity)"></span>
                </span>
            </li>
            <li>
               <span class="weather-item -lovelace" ng-if="item.fields.sunrise">
                  <i class="mdi mdi-weather-sunset-up"></i>
                  <span ng-bind="getWeatherField('sunrise', item, entity)">
                  </span>
                </span>
            </li>
            <li>
               <span class="weather-item -lovelace" ng-if="item.fields.windBearing">
                  <span ng-bind="getWeatherField('windBearing', item, entity)">
                  </span>
                </span>
            </li>
            <li>
               <span class="weather-item -lovelace" ng-if="item.fields.sunset">
                  <i class="mdi mdi-weather-sunset-down"></i>
                  <span ng-bind="getWeatherField('sunset', item, entity)"></span>
                </span>
            </li>
	    </ul>
	    </div>
            <div class="weather-line -lovelace" ng-repeat="line in item.fields.list">
               <span ng-bind="getWeatherLine(line, item, entity)"></span>
            </div>

         </div>
    `,
};

var get_weather_forecast_list = function(id,mode){
        var provider="weather.gismeteo";
        if (mode == 'daily') provider +="_daily";
	var condition= function() {
    	    return this.states[provider].attributes.forecast[id].condition;
        };
	var temperature= function() {
    	    return this.states[provider].attributes.forecast[id].temperature + " °";
        };
	var templow= function() {
    	    return this.states[provider].attributes.forecast[id].templow ?
    		this.states[provider].attributes.forecast[id].templow + " °"
    		: "---";
        };
	var wind_speed= function() {
    	    return (parseFloat(this.states[provider].attributes.forecast[id].wind_speed) / 3.6).toFixed(0) + " м/с";
        };
	var wind_bearing= function() {
	    var bearing_raw = this.states[provider].attributes.forecast[id].wind_bearing;
	    if (isNaN(bearing_raw)){
		return "--";
	    } else {
	        return windDirections_ru[parseInt((bearing_raw + 11.25) / 22.5)];
	    };
        };
	var precipitation= function() {
	    var precip_raw = this.states[provider].attributes.forecast[id].precipitation;
	    if ((isNaN(precip_raw))||(!precip_raw)){
		return "0 мм";
	    } else {
    	        return precip_raw + " мм";
    	    };
        };
      var date2;
      const lang = this.selectedLanguage || this.language;
      if (mode == 'hourly'){
        date2 = function () {
        	return new Date(
            	    this.states[provider].attributes.forecast[id].datetime
            	    ).toLocaleTimeString(lang,{
            		hour: '2-digit', minute: '2-digit'
            	    });
            	}
      } else {
        date2 = function () {
        	return new Date(
            	    this.states[provider].attributes.forecast[id].datetime
            	    ).toLocaleDateString(lang,{
            		month: 'short', day: 'numeric'
            	    });
            	}
      }
      return {
         date: function () {
            return new Date(
               this.states[provider].attributes.forecast[id].datetime
               ).toLocaleDateString(lang,{
                weekday: "short"
               });
         },
         date2: date2,
	 icon: condition,
	 temperature: temperature,
	 templow: templow,
	 wind_speed: wind_speed,
	 wind_bearing: wind_bearing,
	 precipitation: precipitation,
	 
         primary: condition,
         secondary: date2,
      }
}

var weather_forecast_list_html_lovalace = `
         <div class="weather-list -lovelace">
            <table>
        	<tr>
        	    <td ng-repeat="line in item.list track by $index">
                       <div class="weather-list-dayname -lovelace" ng-bind="weatherListField('date', line, item, entity)"></div>
                       <div class="weather-list-dayname_second -lovelace" ng-bind="weatherListField('date2', line, item, entity)"></div>
                       <div class="weather-list-icon-container -lovelace">
                         <div class="weather-list-icon -lovelace"
                           ng-if="(_icon = weatherListIcon(line, item, entity))">
                            <div class="wu " ng-class="'wu-' + _icon"></div>
                         </div>
                         <div class="weather-list-icon-image -lovelace"
                           ng-if="(_imgStyles = weatherListImageStyles(line, item, entity))">
                            <div ng-style="_imgStyles"></div>
                         </div>
		       </div>
                       <div class="weather-list-highTemp -lovelace" ng-bind="weatherListField('temperature', line, item, entity)"></div>
                       <div class="weather-list-lowTemp -lovelace" ng-bind="weatherListField('templow', line, item, entity)"></div>
                       <div class="weather-list-precipitation -lovelace" ng-bind="weatherListField('wind_speed', line, item, entity)"></div>
                       <div class="weather-list-precipitation -lovelace" ng-bind="weatherListField('wind_bearing', line, item, entity)"></div>
                       <div class="weather-list-precipitation -lovelace" ng-bind="weatherListField('precipitation', line, item, entity)"></div>
        	</tr>
            </table>
         </div>

`;

var CONFIG = {
/* .... */

   pages: [
      {
         title: 'Main page',
         bg: 'images/bg1.jpeg',
         icon: 'mdi-home-outline', // home icon
         groups: [
            {
               //title: 'First group',
               width: 3,
               height: 3,
               items: [
                    angular.merge(weather_current,{
                	position: [0, 0],
                	height: 1,
                	width: 3,
                    }),
                    {
                	position: [0, 1],
                	width: 3,
                	height: 1,
			type: TYPES.CUSTOM,
			id: {},
			icons: angular.merge(weather_icons,{}),
			list: [0,1,2,3,4].map(function (id) { return get_weather_forecast_list(id,'hourly');}),
			customHtml: angular.merge(weather_forecast_list_html_lovalace,{}),
                    },
                    {
                	position: [0, 2],
                	width: 3,
                	height: 1,
			type: TYPES.CUSTOM,
			id: {},
			icons: angular.merge(weather_icons,{}),
			list: [0,1,2,3,4].map(function (id) { return get_weather_forecast_list(id,'daily');}),
			customHtml: angular.merge(weather_forecast_list_html_lovalace,{}),
                    },
               ]
            },
/* ... */
   ],
}

ah… and related custom.css part


.variations {
    display: flex;
    flex-flow: row wrap;
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: space-between;
    align-content: normal;
    font-weight: 300;
    color: var(--primary-text-color);
    list-style: none;
    padding: 0 1em;
    margin: 0;
}
.variations li {
    flex-basis: auto;
    width: 33%;
}
.weather-list-icon.-lovelace > div,
.weather-list-icon-image.-lovelace > div {
  width: 25px;
  height: 25px;
}
.weather-list-dayname.-lovelace {
  font-weight: bold;
  text-transform: capitalize;
}
.weather-list-dayname_second.-lovelace {
  font-weight: 300;
}
.weather-list-highTemp.-lovelace {
  font-weight: bold;
}
.weather-list-lowTemp.-lovelace {
  font-weight: 300;
}
.weather-list-precipitation.-lovelace {
  font-weight: 300;
}
.weather-list.-lovelace table td:nth-child(even) {
  background: rgba(11, 11, 11, 0.11);
}
.weather-list.-lovelace table th:first-of-type,
.weather-list.-lovelace table td:first-of-type {
  padding-left: 0;
}
.weather-list.-lovelace table th:last-of-type,
.weather-list.-lovelace table td:last-of-type {
  padding-right: 0;
}
.weather-list.-lovelace table th {
  padding: 0 0;
}
.weather-list.-lovelace table td {
  padding: 0 0;
  margin: 0 0 0 0;
  width: 20%;
}
.weather-list.-lovelace table {
  text-align: center;
  font-size: 15px;
}
.weather-item.-lovelace {
  font-size: 1.5em;
  font-weight: 300;
}
.weather-line.-items.-lovelace {
  margin-bottom: 0;
}

.weather-temperature.-lovelace {
  font-weight: 400;
  font-size: 65px;
  margin-right: 30px;
  margin-bottom: 0;
  text-align: right;
}

.weather-temperature-unit.-lovelace {
  font-weight: 400;
  font-size: 30px;
  vertical-align: super;
  margin-top: -5px;
  text-align: left;
  position: absolute;
}
.weather-icon.-lovelace > div,
.weather-icon-image.-lovelace > div {
  width: 80px;
  height: 80px;
  position: absolute;
  left: 0;
}
.weather-icon.-lovelace,
.weather-icon-image.-lovelace{
  margin: 0 auto 0;
}

Got

Uncaught (in promise) DOMException: play() failed because the user didn't interact with the document first

and video stopped streaming.

Added el.muted = 'muted'; fixed the issue.

...
var el = document.createElement('video');
el.style.width = '100%';
el.muted = 'muted';
...

I’m using a iPad 2 as a console, any tips on working out the tile size, spacing, etc for best use of the resolution?

Does anyone know how to add a third column to Weather_list?

THis is what i have:

{
position: [1, 1],
type: TYPES.WEATHER_LIST,
width: 3,
height: 3,
title: ‘Forecast’,
id: ‘group.weather’,
icons: {
‘clear-day’: ‘clear’,
‘clear-night’: ‘nt-clear’,
‘cloudy’: ‘cloudy’,
‘rain’: ‘rain’,
‘sleet’: ‘sleet’,
‘snow’: ‘snow’,
‘wind’: ‘hazy’,
‘fog’: ‘fog’,
‘partly-cloudy-day’: ‘partlycloudy’,
‘partly-cloudy-night’: ‘nt-partlycloudy’
},
hideHeader: false,
secondaryTitle: ‘Wind’,
thirdTitle: ‘Precipitation’,
list: [1,2,3,4,5].map(function (id) {
var forecast = “&sensor.dark_sky_overnight_low_temperature_” + id + "d.state - “;
forecast += “&sensor.dark_sky_daytime_high_temperature_” + id + “d.state”;
forecast += “&sensor.dark_sky_daytime_high_temperature_” + id + “d.attributes.unit_of_measurement”;
var wind = “&sensor.dark_sky_wind_speed_” + id + “d.state”;
wind += " &sensor.dark_sky_wind_speed_” + id + “d.attributes.unit_of_measurement”;

					var precip = "&sensor.dark_sky_precip_probability_" + id + "d.state - ";
					precip += "&sensor.dark_sky_precip_probability_" + id + "d.attributes.unit_of_measurement";
					
					return {
				date: function () {
					var d = new Date(Date.now() + id * 24 * 60 * 60 * 1000);
					return d.toString().split(' ').slice(1, 3).join(' ');
				  },
				icon: "&sensor.dark_sky_icon_" + id + ".state",
				//iconImage: null, replace icon with image
				primary: forecast,
				secondary: wind, precip,
				third: precip,
				}
			})
			},
           ]

this is what it looks like:

image

I would rather have a forcast like (for 5 days)
image
If someone could help out with the code. Im still learning

Thank you! I will give it a try (when I have the time :wink: )

ok. For closing this for a while. As already said using ‘bind-html-compile’ from https://github.com/incuna/angular-bind-html-compile
instead of ‘ng-bind-html’ allow to use ng- directives inside of customHtml.
I aslo added “nowatch” version as descibed here: https://stackoverflow.com/a/20430478
in case when customHtml need to be evaluated only once.
For PopUp frames i’ve added ‘customPopupHtml’ field, it’s allowed to be a function() too. For simplifying changes I’ve use POPUP_IFRAME for displayin this customPopupHtml.
It’s rendered before ‘url’. If both ‘customPopupHtml’ and ‘usr’ specified, then frame display both.
final patch is here:

diff --git a/index.html b/index.html
index 11ca2d8..b272a41 100644
--- a/index.html
+++ b/index.html
@@ -51,6 +51,8 @@
          'scripts/vendors/angular-chart.min.js',  // Required for linking charts to angular.js
          'scripts/vendors/hls.js',
 
+         'scripts/vendors/angular-bind-html-compile.min.js',
+         'scripts/vendors/angular-bind-html-compile-once.min.js',
          'scripts/models/noty.js',
          'scripts/app.js',
 
@@ -128,7 +130,10 @@
             </div>
             {{ entityTitle(activeIframe, entity) }}
          </div>
-         <div class="iframe-popup--iframe">
+         <div class="iframe-popup--html" ng-if="activeIframe.customPopupHtml"
+                       bind-html-compile = "itemCustomPopupHtml(activeIframe, entity)">
+         </div>
+         <div class="iframe-popup--iframe" ng-if="activeIframe.url">
             <iframe ng-src="{{ itemField('url', activeIframe, entity) }}"
                     iframe-tile="activeIframe" frameborder="0"></iframe>
          </div>
@@ -641,7 +646,7 @@
            class="item-entity-container">
 
          <div ng-if="item.customHtml"
-              ng-bind-html="itemCustomHtml(item, entity)"></div>
+              bind-html-compile-once="itemCustomHtml(item, entity)"></div>
 
          <div ng-if="!item.customHtml" class="item-entity">
             <span class="item-entity--icon mdi"
diff --git a/scripts/controllers/main.js b/scripts/controllers/main.js
index fc61899..f6e3575 100755
--- a/scripts/controllers/main.js
+++ b/scripts/controllers/main.js
@@ -528,6 +528,14 @@ App.controller('Main', ['$scope', '$timeout', '$location', 'Api', function ($sco
       return item.customHtml;
    };
 
+   $scope.itemCustomPopupHtml = function (item, entity) {
+      if (typeof item.customPopupHtml === 'function') {
+         return callFunction(item.customPopupHtml, [item, entity]);
+      }
+
+      return item.customPopupHtml;
+   };
+
    $scope.entityUnit = function (item, entity) {
       if(!('unit' in item)) {
          return entity.attributes ? entity.attributes.unit_of_measurement : null;
diff --git a/scripts/init.js b/scripts/init.js
index 1ea7d32..2933d23 100644
--- a/scripts/init.js
+++ b/scripts/init.js
@@ -5,7 +5,7 @@ if(!window.CONFIG) {
    alert(error);
 }
 
-var App = angular.module('App', ['hmTouchEvents', 'colorpicker', 'angularjs-gauge', 'chart.js']);
+var App = angular.module('App', ['hmTouchEvents', 'colorpicker', 'angularjs-gauge', 'chart.js', 'angular-bind-html-compile', 'angular-bind-html-compile-once']);
 
 App.config(function($sceProvider, $locationProvider, ApiProvider, ChartJsProvider) {
    $sceProvider.enabled(false);

Would be nice if this will be included in master branch.

Has anyone figured out how set ‘step’ for TYPE.CLIMAT? Now my thermostate increases or decreases by a whole degree instead of an half.

Speed issue:
In my config i have a Weather block with ~ 90 variables. This cause a problem on old tables devices. Because each state change in HomeAssistant became UpdateView in Tileboard. It you have lot of entityes in HA, this update called very often.
On each update, AngulaJS re-read all binded variables. In case of Tileboard this mean to call all function stack (getWeatherField -> parceFieldValue -> parseString -> parseVariable … etc., and worse if you have a function() as a field value) for the each of variables.
So, multiply it to 90 variables, then multiply to some times per second of “state_changed” event, and you’ll see why my switch tiles works with delay of 2 seconds (average), and another two seconds for display new state.

Main question is: can it be speed up?
I found this: https://dzone.com/articles/why-we-shound-not-use-function-inside-angular-temp but it is for AngularJS 2.x.
I tryed ‘filters’ which are AngularJS 1.x predecessor of ‘pipes’, but no success.

I see workaround with dropping universality and make customHTML with direct addressing to ‘this.states[]’ elements instead of tile object fields.
Other workaround is to filter ‘state_changed’ events on the HA side. perhaps by NodeRed.
But do not like those ways.

The full screen in iOS functionality is not working.
When i pin the page on home screen And open it the address bar still shows up. Anyone has a fix for it?

Forget about this.
I did both: filtered events which trigger updateView and made weather data directly binded from this.states[].
I’ve won ~25% of speed. Now the switch tile change icon after 3 seconds instead of 4.
The device itself is very slow. :frowning:

that is not working on my ipad running ios 13…

i use the exact same settings but i miss the plus and minus buttons to control this climate. what am i doing wrong?

can you please send me how you manage to fix this? i also have 4 tado devices i want to use this way. so show the temp, the current temp and use the + and - buttons.

Thanks, I’ve updated the code.

I am sorry but I haven’t used Tileboard in at least a year. I can no longer answer any questions related to this.

I have other projects running like Homekit Infused which replaced Tileboard in my case. I hope you will find your answer though.

Going to take a look at your app! looks amazing.
is this one also ipad responsive and good to use?

Well, yes and no. It is not an app, it actually is just plain and simple lovelace (obviously not as simple as I project it now :rofl::joy:).

Though it is responsive in design, it is dynamic regarding screensize (buttons will grow/shrink depending on the screensize). But it won’t work on older browsers (like all custom cards btw). So if you rely on ES5 then Tileboard is one of your only options (or go for core cards only).

I am going to release an easy version next month which will fill entities automatically depending on your own entities.
However I will no longer hijack this thread. If you have any questions please post them in the appropriate one.