JS injection for Dashboard

Yesterday I wrote a little driver for injecting JS into Dashboard, as it was a reply to a post in another thread I wrote it there. However, to keep that thread from unrelated talk about JS injection, we now have this thread.

The original post:

With some followup:

And as noted there for Staff: PLEASE don't patch this before allowing us an official way to inject JS into the Dashboard. With JS we can do what we want without requesting a myriad of features for you to develop, if it brakes things, it's our problem for not doing it right. Besides, this way you can, if you wish, see what we the community come up with in terms of features and choose to implement something (or nothing) you think is a good idea and your customers like.

Pinging @spelcheck

11 Likes

I had a few moments over today, this version can retrieve whatever HTML and JS you save in the JSON file for the Dashboard, that way we have no real limitations anymore. See the comment in the source-code for use.

 /**
 *  Copyright 2020 Markus Liljergren
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
metadata {
    definition(name: "JavaScript Injector", namespace: "markus-li", author: "Markus Liljergren") {
        command "refresh"
        command "clear"
        
        attribute "javascript", "string"
        attribute "javascriptLength", "number"
    }
    preferences {   
    }
}


void updated() {
    log.info "Updated..."
    refresh()
}

void refresh() {
    /* EXAMPLE DATA for Dashboard */
    String exampleCSS = '''
.modal {
  font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;
  display: none;
  position: absolute;
  background-color: yellow;
  z-index: 9999;
  top: 50%;
  left: 50%;
  width: 200px;
  height: 200px;
}

.modal.is-open {
  display: block;
}
.modal-close-btn {
  background-color: grey;
}
'''

    /* Insert this EXAMPLE into the JSON for the Dashboard:
    "customCSS": ".modal {\n  font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;\n  display: none;\n  position: absolute;\n  background-color: yellow;\n  z-index: 9999;\n  top: 50%;\n  left: 50%;\n  width: 200px;\n  height: 200px;\n}\n\n.modal.is-open {\n  display: block;\n}\n.modal-close-btn {\n  background-color: grey;\n}\n\nJS {\nvar hi = 2;\ntest = 3;\n}",
    "customJS": "\nvar body = document.getElementsByTagName(\"body\")[0];\nvar script = document.getElementById(\"inserted-body-script\");\nvar hasScript = script != null;\nif(!hasScript) {\nscript = document.createElement(\"script\");\nscript.setAttribute(\"id\", \"inserted-body-script\")\n}\n\nscript.type = \"text/javascript\";\n\nscript.src = \"https://cdn.jsdelivr.net/npm/micromodal/dist/micromodal.min.js\";\nif(!hasScript) {\nbody.appendChild(script);\n//alert(6);\n} else {\nMicroModal.show(\"modal-1\");\n//alert(10);\n}\n\nvar div = document.getElementById(\"inserted-body-html\");\nvar hasDiv = div != null;\nif(!hasDiv) {\ndiv = document.createElement(\"div\")\ndiv.setAttribute(\"id\", \"inserted-body-html\")\n}\n\ndiv.innerHTML = \"\";\nif(!hasDiv) {\nbody.prepend(div);\n}\nscript.onload = function() {\nMicroModal.init({debugMode: true});\nMicroModal.show(\"modal-1\");\nalert(2);\n}",
    "customHTML": "<div id=\"modal-1\" class=\"modal\" aria-hidden=\"true\"><div tabindex=\"-1\" data-micromodal-close><div role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"modal-1-title\" ><header><h2 id=\"modal-1-title\">Modal Title &#229;&#228;&#246;</h2><button class=\"modal-close-btn\" aria-label=\"Close modal\" data-micromodal-close>Close Me</button><img /> <img /></header><div id=\"modal-1-content\">Modal Content</div></div></div></div>",
    */
    
    // The above is just an ugly modal, but it showcases injecting JS and HTML from the JSON config...

    // <div style="display:none;"></title></style></textarea></script></xmp><svg/onload='+/"/+/onmouseover=1/+/[*/[]/+function(){    
    // The below is too many characters, need to use the minified version below
    String myScript = '''
<a href="#" data-micromodal-trigger="modal-1">Open modal dialog2</a>
<svg style="display: none;" onload='
function loadJSON(callback) {
    var urlParams = new URLSearchParams(window.location.search);
    var xobj = new XMLHttpRequest();
        xobj.overrideMimeType("application/json");
    xobj.open("GET", window.location.href + "/layout", true);
    xobj.withCredentials = true;
    xobj.setRequestHeader("Authorization","Bearer " + urlParams.get("access_token"));
    xobj.onreadystatechange = function () {
          if (xobj.readyState == 4 && xobj.status == "200") {
            callback(xobj.responseText);
          }
    };
    xobj.send(null);  
 }
loadJSON(function(response) {
      var data = JSON.parse(response);
      var body = document.getElementsByTagName("body")[0];
      var script = document.getElementById("inserted-bootstrap-script");
      var hasScript = script != null;
      if(!hasScript) {
          script = document.createElement("script");
          script.setAttribute("id", "inserted-bootstrap-script")
      }
      script.type = "text/javascript";
      script.innerHTML = data.customJS;
      if(!hasScript) {
        body.appendChild(script);
      }

      var div = document.getElementById("inserted-bootstrap-html");
      var hasDiv = div != null;
      if(!hasDiv) {
          div = document.createElement("div")
          div.setAttribute("id", "inserted-bootstrap-html")
      }
      div.innerHTML = data.customHTML;
      if(!hasDiv) {
          body.prepend(div);
      }
    });
'></svg>'''
//}()//</div>'>
//'''
    // Minified version of the above
    // https://javascript-minifier.com/
    myScript = '''
<a href="#" data-micromodal-trigger="modal-1">Open md</a>
<svg style="display: none;" onload='
function loadJSON(e){var t=new URLSearchParams(window.location.search),n=new XMLHttpRequest;n.overrideMimeType("application/json"),n.open("GET",window.location.pathname+"/layout",!0),n.withCredentials=!0,n.setRequestHeader("Authorization","Bearer "+t.get("access_token")),n.onreadystatechange=function(){4==n.readyState&&"200"==n.status&&e(n.responseText)},n.send(null)}loadJSON(function(e){console.log("3");var t=JSON.parse(e);console.log(t);var n=document.getElementsByTagName("body")[0],a=document.getElementById("inserted-bootstrap-script"),o=null!=a;o||(a=document.createElement("script")).setAttribute("id","inserted-bootstrap-script"),a.type="text/javascript",a.innerHTML=t.customJS,o||n.appendChild(a);var r=document.getElementById("inserted-bootstrap-html"),s=null!=r;s||(r=document.createElement("div")).setAttribute("id","inserted-bootstrap-html"),r.innerHTML=t.customHTML,s||n.prepend(r)});
'></svg>'''
//}()//</div>'>
//'''
    // now() is to make the string unique each time, but is not needed in production...
    String myJSMsg = "md ${now()} ${myScript}"
    
    sendEvent(name: "javascript", value: "${myJSMsg}", isStateChange: true)
    sendEvent(name: "javascriptLength", value: "${myJSMsg.length()}", isStateChange: true)
    
    log.debug "Now: ${now()}, JS length: ${myJSMsg.length()}, Maximum is 1024"
}

void clear() {
    sendEvent(name: "javascript", value: "No JS", isStateChange: true)
}

void installed() {
    log.info "Installed..."
    refresh()
}

To escape the content for storage in JSON it has to be escaped a little bit, the following is some quick and ugly python to reformat data for insertion, you can use the language of your choice, of course:

# Prepare the HTML

my_html = my_html.replace('"', '\\"').replace('</', '<\/').replace(' />', '/>').replace('/>', ' />')
my_html = my_html.encode('ascii', 'xmlcharrefreplace').decode("ascii")
my_html = ''.join([line.strip() for line in my_html.splitlines()])
print("my_html = '" + my_html + "'")

# Now the JS
my_js = my_js.replace('"', '\\"')
my_js = '\\n'.join([line.strip() for line in my_js.splitlines()]).replace("\r", "").replace("\t", "")
print("my_js = '" + my_js + "'")
5 Likes

I'm stunned! :star_struck:

2 Likes

Here's a different version which can double as a "temperature" tile when used as an attribute tile with the attribute "javascript" selected.

 /**
 *  Copyright 2020 Markus Liljergren
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
import java.util.Date

metadata {
    definition(name: "JavaScript Injector", namespace: "markus-li", author: "Markus Liljergren") {
        capability "TemperatureMeasurement"
        capability "Refresh"

        command "clear"
        command "setTemperature", [[name: "NUMBER", type: "NUMBER"]]
        
        attribute "javascript", "string"
        //attribute "javascriptLength", "number"
    }
    preferences {
      input(name: "useDegreeC", type: "bool", title: "Use degrees C&deg;? (Off for Degrees F&deg;)", description: "Degrees C&deg;", defaultValue: false, displayDuringSetup: false, required: false)
    }
}


void updated() {
    log.info "updated()"
    refresh()
}

void refresh() {
  log.info "refresh()"
  /* EXAMPLE DATA for Dashboard */
  String exampleCSS = '''
.modal {
  font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;
  display: none;
  position: absolute;
  background-color: yellow;
  z-index: 9999;
  top: 50%;
  left: 50%;
  width: 200px;
  height: 200px;
}

.modal.is-open {
  display: block;
}
.modal-close-btn {
  background-color: grey;
}

#open-modal-btn {
position: absolute;
background-color: whitesmoke;
top: 10px;
left: 400px;
}
'''

  /* Insert this EXAMPLE into the JSON for the Dashboard:
  "customCSS": ".modal {\n  font-family: -apple-system,BlinkMacSystemFont,avenir next,avenir,helvetica neue,helvetica,ubuntu,roboto,noto,segoe ui,arial,sans-serif;\n  display: none;\n  position: absolute;\n  background-color: yellow;\n  z-index: 9999;\n  top: 50%;\n  left: 50%;\n  width: 200px;\n  height: 200px;\n}\n\n.modal.is-open {\n  display: block;\n}\n.modal-close-btn {\n  background-color: grey;\n}\n\n#open-modal-btn {\nposition: absolute;\nbackground-color: whitesmoke;\ntop: 10px;\nleft: 400px;\n}",
  "customJS": "\nvar body = document.getElementsByTagName(\"body\")[0];\nvar script = document.getElementById(\"inserted-body-script\");\nvar hasScript = script != null;\nif(!hasScript) {\nscript = document.createElement(\"script\");\nscript.setAttribute(\"id\", \"inserted-body-script\")\n}\n\nscript.type = \"text/javascript\";\n\nscript.src = \"https://cdn.jsdelivr.net/npm/micromodal/dist/micromodal.min.js\";\nif(!hasScript) {\nbody.appendChild(script);\n//alert(6);\n} else {\nMicroModal.show(\"modal-1\");\n//alert(10);\n}\n\nvar div = document.getElementById(\"inserted-body-html\");\nvar hasDiv = div != null;\nif(!hasDiv) {\ndiv = document.createElement(\"div\")\ndiv.setAttribute(\"id\", \"inserted-body-html\")\n}\n\ndiv.innerHTML = \"\";\nif(!hasDiv) {\nbody.prepend(div);\n}\nscript.onload = function() {\nMicroModal.init({debugMode: true});\nMicroModal.show(\"modal-1\");\nalert(2);\n}",
  "customHTML": "<div id=\"open-modal-btn\"><a href=\"#\" data-micromodal-trigger=\"modal-1\">Open Modal</a></div><div id=\"modal-1\" class=\"modal\" aria-hidden=\"true\"><div tabindex=\"-1\" data-micromodal-close><div role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"modal-1-title\" ><header><h2 id=\"modal-1-title\">Modal Title &#229;&#228;&#246;</h2><button class=\"modal-close-btn\" aria-label=\"Close modal\" data-micromodal-close>Close Me</button><img /> <img /></header><div id=\"modal-1-content\">Modal Content</div></div></div></div>",
  */
  
  // The above is just an ugly modal, but it showcases injecting JS and HTML from the JSON config...

  // The below is too many characters, need to use the minified version below
  String jsInjectionWithReInsert = '''
<svg style="display: none;" onload='
function lJ(callback) {
    var urlParams = new URLSearchParams(window.location.search);
    var xobj = new XMLHttpRequest();
        xobj.overrideMimeType("application/json");
    xobj.open("GET", window.location.pathname + "/layout", true);
    xobj.withCredentials = true;
    xobj.setRequestHeader("Authorization","Bearer " + urlParams.get("access_token"));
    xobj.onreadystatechange = function () {
          if (xobj.readyState == 4 && xobj.status == "200") {
            callback(xobj.responseText);
          }
    };
    xobj.send(null);  
 }
lJ(function(response) {
  console.log(1);
      var data = JSON.parse(response);
      var body = document.getElementsByTagName("body")[0];
      var div = document.getElementById("inserted-bootstrap-html");
      var hasDiv = div != null;
      if(!hasDiv) {
          div = document.createElement("div");
          div.setAttribute("id", "inserted-bootstrap-html");
      }
      div.innerHTML = data.customHTML;
      if(!hasDiv) {
          body.prepend(div);
      }

      var script = document.getElementById("inserted-bootstrap-script");
      var hasScript = script != null;
      if(script != null) {
          script.remove();
      }
      script = document.createElement("script");
      script.setAttribute("id", "inserted-bootstrap-script")
      script.type = "text/javascript";
      script.innerHTML = data.customJS;
      body.prepend(script);
console.log("1E");
    });
'></svg>'''

  String jsInjectionWithoutReInsert = '''
<svg style="display: none;" onload='
function lJ(callback) {
    var urlParams = new URLSearchParams(window.location.search);
    var xobj = new XMLHttpRequest();
        xobj.overrideMimeType("application/json");
    xobj.open("GET", window.location.pathname + "/layout", true);
    xobj.withCredentials = true;
    xobj.setRequestHeader("Authorization","Bearer " + urlParams.get("access_token"));
    xobj.onreadystatechange = function () {
          if (xobj.readyState == 4 && xobj.status == "200") {
            callback(xobj.responseText);
          }
    };
    xobj.send(null);  
 }
lJ(function(response) {
      var data = JSON.parse(response);
      var body = document.getElementsByTagName("body")[0];
      var div = document.getElementById("inserted-bootstrap-html");
      if(div == null) {
          div = document.createElement("div");
          div.setAttribute("id", "inserted-bootstrap-html");
          div.innerHTML = data.customHTML;
          body.prepend(div);
      }
      var script = document.getElementById("inserted-bootstrap-script");
      if(script == null) {
          script = document.createElement("script");
          script.setAttribute("id", "inserted-bootstrap-script");
          script.type = "text/javascript";
          script.innerHTML = data.customJS;
          body.prepend(script);
      }
    });
'></svg>'''

  // Minified version of the above
  // https://javascript-minifier.com/
  jsInjectionWithReInsert = '''
<svg style="display: none;" onload='
function lJ(e){var t=new URLSearchParams(window.location.search),n=new XMLHttpRequest;n.overrideMimeType("application/json"),n.open("GET",window.location.pathname+"/layout",!0),n.withCredentials=!0,n.setRequestHeader("Authorization","Bearer "+t.get("access_token")),n.onreadystatechange=function(){4==n.readyState&&"200"==n.status&&e(n.responseText)},n.send(null)}lJ(function(e){console.log(1);var t=JSON.parse(e),n=document.getElementsByTagName("body")[0],o=document.getElementById("inserted-bootstrap-html"),r=null!=o;r||(o=document.createElement("div")).setAttribute("id","inserted-bootstrap-html"),o.innerHTML=t.customHTML,r||n.prepend(o);var a=document.getElementById("inserted-bootstrap-script");null!=a&&a.remove(),(a=document.createElement("script")).setAttribute("id","inserted-bootstrap-script"),a.type="text/javascript",a.innerHTML=t.customJS,n.prepend(a),console.log("1E")});
'></svg>'''

  jsInjectionWithoutReInsert = '''
<svg style="display: none;" onload='
function lJ(e){var t=new URLSearchParams(window.location.search),n=new XMLHttpRequest;n.overrideMimeType("application/json"),n.open("GET",window.location.pathname+"/layout",!0),n.withCredentials=!0,n.setRequestHeader("Authorization","Bearer "+t.get("access_token")),n.onreadystatechange=function(){4==n.readyState&&"200"==n.status&&e(n.responseText)},n.send(null)}lJ(function(e){var t=JSON.parse(e),n=document.getElementsByTagName("body")[0],r=document.getElementById("inserted-bootstrap-html");null==r&&((r=document.createElement("div")).setAttribute("id","inserted-bootstrap-html"),r.innerHTML=t.customHTML,n.prepend(r));var a=document.getElementById("inserted-bootstrap-script");null==a&&((a=document.createElement("script")).setAttribute("id","inserted-bootstrap-script"),a.type="text/javascript",a.innerHTML=t.customJS,n.prepend(a))});
'></svg>'''
    
  String myJSMsg = "Refreshed(${(new Date()).format("ss")})${jsInjectionWithReInsert}"
  
  sendEvent(name: "javascript", value: "${myJSMsg}", isStateChange: true)
  //sendEvent(name: "javascriptLength", value: "${myJSMsg.length()}", isStateChange: true)
  
  log.debug "Now: ${now()}, JS length: ${myJSMsg.length()}, Maximum is 1024"
}

void setTemperature(BigDecimal number) {
  // This version doesn't reload the JS every time the temperature is updated
  String jsInjectionWithoutReInsert = '''
<svg style="display: none;" onload='
function lJ(e){var t=new URLSearchParams(window.location.search),n=new XMLHttpRequest;n.overrideMimeType("application/json"),n.open("GET",window.location.pathname+"/layout",!0),n.withCredentials=!0,n.setRequestHeader("Authorization","Bearer "+t.get("access_token")),n.onreadystatechange=function(){4==n.readyState&&"200"==n.status&&e(n.responseText)},n.send(null)}lJ(function(e){var t=JSON.parse(e),n=document.getElementsByTagName("body")[0],r=document.getElementById("inserted-bootstrap-html");null==r&&((r=document.createElement("div")).setAttribute("id","inserted-bootstrap-html"),r.innerHTML=t.customHTML,n.prepend(r));var a=document.getElementById("inserted-bootstrap-script");null==a&&((a=document.createElement("script")).setAttribute("id","inserted-bootstrap-script"),a.type="text/javascript",a.innerHTML=t.customJS,n.prepend(a))});
'></svg>'''
  // &deg; doesn't work for the Unit field
  String unitDegree = "${(char)176}F"
  if(useDegreeC == true) {
    unitDegree = "${(char)176}C"
  }
  String myJSMsg = "<span class=\"jsi-temp\">$number <span class=\"small\"><span> $unitDegree </span></span></span>${jsInjectionWithoutReInsert}"
  sendEvent(name: "temperature", value: "${number}", unit: unitDegree)
  sendEvent(name: "javascript", value: "${myJSMsg}", unit: unitDegree)
  //sendEvent(name: "javascriptLength", value: "${myJSMsg.length()}")
  
  //log.debug "Now: ${now()}, JS length: ${myJSMsg.length()}, Maximum is 1024"
}

void clear() {
    sendEvent(name: "javascript", value: "No JS", isStateChange: true)
}

void installed() {
    log.info "Installed..."
    refresh()
}

It can also be hidden by setting "display:none" in the CSS for the tile id of the virtual device with this driver. The JSON should then be updated with something like this and you can have another tile on top of it:

    {
      "rowSpan": 1,
      "template": "attribute",
      "col": 1,
      "colSpan": 1,
      "id": 13,
      "row": 1,
      "device": "1121",
      "templateExtra": "javascript"
    },

This is not a simple thing to use, but for those that want JS to work I assume the above isn't too hard. I know @spelcheck is working hard on getting this to be more user friendly. Looking forward to seeing the results!

3 Likes

Cool. I figured you could do this because I have already added a base64 image to a driver and shown it on a dashboard. In the back of my head JS was going to be next but I just hadn't gotten around to it yet. I'm not ready to work on looks yet. I'm still nailing functionality down.

Yes, I've had it on my list of future things to implement, when I saw what @spelcheck was doing I thought it might be time to write it. There is a limitation of 1024 characters for attributes, which is why this driver just does the bare minimum needed just to get the injection started.

1 Like

Noob alert....

If I simply want to include a few html separators thusly...


... in a dashboard, is it possible with the above? Struggling to follow it and understand what to do exactly. How to go about doing that?

Short answer, yes, it can be done. Long answer, you need to know how to use JS to manipulate the DOM. Jquery is not loaded by default in the Dashboard, so you have plain JS to contend with, unless you load in Jquery, which I'm at this point not sure can be done without unforeseen consequences.

EDIT: @Angus_M For a more complete answer, in the source code, there is an example. What you basically need to do is write whatever JS you want, put it inside a field named customJS in the JSON for the Dashboard (after escaping it, there is no code included that does that). I did it in a simple Python script:

 # Prepare the HTML
 my_html = my_html.replace('"', '\\"').replace('</', '<\/').replace(' />', '/>').replace('/>', ' />')
 my_html = my_html.encode('ascii', 'xmlcharrefreplace').decode("ascii")
 my_html = ''.join([line.strip() for line in my_html.splitlines()])
 print("my_html = '" + my_html + "'")
 
 # Now the JS
 my_js = my_js.replace('"', '\\"')
 my_js = '\\n'.join([line.strip() for line in my_js.splitlines()]).replace("\r", "").replace("\t", "")
 print("my_js = '" + my_js + "'")

This really isn't a complete thing, it is a proof of concept, it can be used, but it is not user friendly. It requires a lot (or at least some) of JS knowledge. A way to use this in a user-friendly manner is being worked on, but it will take time before the amount of time needed to get there has been spent. I'd be happy to try to give more pointers, but it is as I said, not yet very user friendly.
If I would add some way of auto-escaping the JS code and pointers on where to begin with manipulating the DOM maybe it would be better, just that I have too much going on as it is. If any dev wants to use this, please do! This I put here for devs who wants to use it to do cool stuff with the Dashboard. I think for, at least, a few of the devs they knew they could do this, just never got around to it, yet.
I hope this helps you, I don't mean to be short, it's just that it really isn't as much a release as it is just some code that can be used to get the JS in there. If you have some more specific questions on the way to get this to work, ask here and I will try to answer.

3 Likes

I wanted to find a way to make this work without running into the character limit for dashboard tiles or needing to add the (manually escaped) JavaScript to the JSON file and still allow a larger JavaScript file to be injected onto any dashboard, and optionally multiple JavaScript files could be injected per dashboard depending on need.

You can inject 1 JavaScript file per device that you create with this driver. This device can be included in as many dashboards as you want, but each device should only be added 1 time to each dashboard. create additional devices for additional JavaScript files.

I have created this device driver:
https://raw.githubusercontent.com/michaelbarone/hubitat/master/drivers/dashboardJavaScriptInjector.groovy

create a virtual device and apply this driver. Then you can add the url to any JavaScript file you would like to include on a dashboard. There is another preference to delay the loading the of the JavaScript incase your dashboard is not fully loaded by the time the JavaScript loads and starts executing, default is 500 milliseconds, or set to 0 to disable the delay.

Add this device to your dashboard, and choose the "Attribute" option and select "JavaScript". Apply the following CSS to this device tile so it does not show on your dashboard:

/* JavaScript Injector Device */
/* replace '#tile-33' with this device on your dashboard */
#tile-33 {
	display:none;
}

In my use case and tests, I have uploaded a JavaScript file to the file system on the hubitat hub. This will only be available locally, which is fine for me as I do not use dashboards when outside my network/vpn. If you want this to work outside your network, you will need to host this JavaScript file on a webserver that is publicly available on the internet, you might be able to use github for hosting, I have not tried this.

Here is my example JavaScript file, which is setup to run a function on dashboard load and has a logic to replace text strings if found in any tile-title fields.
https://raw.githubusercontent.com/michaelbarone/hubitat/master/drivers/dashboardJavaScriptInjector-Scripts/dash-JS-inject1.js

To ensure the script is getting injected on your dashboard, you should be able to see the script near the bottom the of the iframe in the browser dev tools. The id of the script will be set to your device name that injected the script, so its easy to find/manage:

The JavaScript will also console.log a message when on the device/edit page, so you can make sure its working, but the injected JavaScript will not execute on the device/edit page:

2 Likes

Hi Marcus / mbarone
Just thought I'd post and say thanks for this excellent driver as it's enabled me to create a somewhat complex interactive SVG based dashboard as you can see here.
The ability of utilising Javascript that your driver has given really does open up a world of opportunities. I'm surprised that Hubitat haven't added the ability to incorporate custom JS into dashboards as standard.
So thank you very much for posting it.
Dave

3 Likes

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.