How to combine 2 Hubitat Devices into single Custom HTML tile?

I don’t have much experience with the code for custom tiles. I did my best but can’t get it to work. How might one modify the custom HTML tile to combine multiple hubitat devices into a single custom tile? Here’s my attempt…

<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="//cdn.sharptools.io/js/custom-tiles.js"></script>

<script>
  //stub a variable to hold the settings at a higher scope
  // var settings; //apiSample, deviceId, attribute
  var tileSettings = { 
    apiSample: "XXXXXXXX", 
    deviceId1: "2374",
    attribute1: "chart",
    deviceId2: "2146",
    attribute2: "chart",    
  }
  var apiSettings = {};
  var timerId;
//  var REFRESH_INTERVAL = 60 * 60 * 1000; //every 60 minutes?
   var REFRESH_INTERVAL = 3 * 60 * 1000; // every 3 minutes
  //get the content element so we can update it later
  var contentEl = document.getElementById("content1");
  var contentE2 = document.getElementById("content2");
  //when the tile is ready
  stio.ready(function(data){
    //get the settings from the callback
    // tileSettings = data.settings; //token, deviceId, attribute
    //and initialize the tile
    init();
  });

  var tapState = 0

  function init(){
    //parse the relevant settings out of the apiSample
    parseApiSample();
    //make a call to the Hubitat API to get some data
    refresh().then(function(){
      //if the first refresh is successful, schedule the periodic refreshes
      timerId = setInterval(refresh, REFRESH_INTERVAL);
    });

  }

  //helper method for logging an error to console, showing a toast, and updating the tile to display 'Error'
  function showError(message){
    console.error(message);
    stio.showToast(message, "red");
    contentEl.innerText = "Error";
    contentE2.innerText = "Error";
  }

  //parse out the various components from a provided Maker API URL
  function parseApiSample(sampleUrl){
    //if we weren't passed in an explicit URL to parse
    if(sampleUrl == null){
      //then try to use the sample API URL from the tile settings
      sampleUrl = tileSettings.apiSample;
    }
    //if no URL was provided, let the user know
    if(sampleUrl == null || sampleUrl === "") showError("No API URL was provided. Please configure the tile.");
    //if the api isn't the cloud API, let the user know
    if(sampleUrl.indexOf("cloud.hubitat.com") < 0) showError("Please use the Hubitat Maker API CLOUD URI.")

    //try to parse out the various parts of the URL we need with a regular expression
    var re = /https:\/\/cloud\.hubitat\.com\/api\/([^\/]+)\/apps\/([\d]+)\/[^\?]+\?access_token\=([^\&]+)/;
    var match = sampleUrl.match(re); //array of the various regex matches (0: full string, 1: hub id, 2: app id, 3: token)

    //pass the parsed settings back into the top-level variable
    apiSettings = { 
      "hubId": match[1], 
      "appId": match[2], 
      "token": match[3]
    };
  }

  //helper function to format the parsed data back into a base URI
  function getBaseUrl(){
    return `https://cloud.hubitat.com/api/${apiSettings.hubId}/apps/${apiSettings.appId}`
  }
  //helper function to format the token into an axios 'data' object to attach the token as a parameter
  function getAxiosConfig(){
    return {params: {access_token: apiSettings.token}};
  }

  function refresh(){
    let thing1 = getThing(1)
    let thing2 = getThing(2)
      //try to find the desired parameter
        let attribute1 = thing1.attributes.find(function(attr){ return attr.name === tileSettings.attribute1});
        let attribute2 = thing2.attributes.find(function(attr){ return attr.name === tileSettings.attribute2});

      //if we didn't get the attribute, bail out
        if(attribute1 == null || attribute2 == null) return showError("Could not find desired attribute");
      console.log('got attribute1', attribute1);
      console.log('got attribute2', attribute2);
    //  showError(attribute.currentValue)
      //otherwise inject the attribute value as HTML
      var contentEl = document.getElementById("content1");
        contentEl.innerHTML = attribute1.currentValue
      var contentE2 = document.getElementById("content2");
        contentE2.innerHTML = attribute2.currentValue      
      event.preventDefault()
  }

  function getThing(num){
    var deviceId = tileSettings.deviceId1
    if (num == 2) deviceId = tileSettings.deviceId2
    let url = getBaseUrl() + `/devices/${deviceId}` + `?access_token=` + apiSettings.token
    let config = getAxiosConfig();
    //make the API call
    return axios.get(url).then(function(response){
      //if we got a response with the expected base data
      if(response.data && response.data.attributes){
        //return the response
        return response.data; //TODO: we could parse it into a more helpful "Thing" object format
      }
    }).catch(function(error){
      showError("Error communicating with Maker API with url of:" + url)
    })
  }
</script>

<style>
  html,body {height: 100%;margin:0;}
  /*
  .main-content {
    display: flex; 
    height:100%;
    align-items: center;
    justify-content: center;
  }
  */
  /*
  .main-content #content {
    text-align: center; /* OPTIONAL center align any text that gets injected */
  }
  /*
  .main-content #content img {
    max-width: 100%; /* OPTIONAL scale any inner images if they're too big */
  }
  */
  */
  /* OPTIONAL APPROACH FOR KNOWN CONTENT FORMATS
     (Uncomment below CSS to use)
  
     Another vertical auto-scale approach 
     where the expected inner content from 
     the attribute is known.
  
     Makes the 'content' holder flex so 
     we can center and forces the content 
     into a column.

     Then restricts the to a maximum height
     and hides the br since it shouldn't
     display in flex layout.
  */
  /*
  #content { 
    display: flex; 
    flex-direction: column;
    height: 100%; 
    width: 100%;
    align-items: center;
    justify-content: center;
  }
  #content img {
    max-height: 70%; 
  }
  #content br { display: none; }
  */
</style>
<div class="main-content" id="main-content">
  <span id="content1"></span>
  <span id="content2"></span>
</div>

If you just want some attributes from different devices you can make a “Super Tile”

Right, but the attributes here are html, so I have to use a custom tile. Can’t combine two html attributes in a super tile.

I think the issue is with the getThing() call which is returning a promise that you would need to await the results of.

You can either Promise.all() the two promises or use async-await if you’re only targeting newer browsers.

If you give me a bit, I’ll try to stub the Promise.all() approach as an example for you. :slight_smile:

Sure, that would be awesome. I’m really not familiar with Axios (as you can probably tell)…

Here’s an updated snippet.

So the main difference is, it awaits both of the promises results before continuing. You can see how that works by looking at the diff if you’re interested.

Edit: I just realized I used an arrow function in the Promise.all().then(). If you’re using an older browser, you can swap that out to a proper function call. (And if that doesn’t make sense, let me know and I can edit the example for you :smiley: )

Ah! That makes sense. Thanks!!

I have this working, but it’s not refreshing when it should be. Does something need to change with the refresh code too?
EDIT: not sure if the “arrow function” you mentioned above is the issue, because it works with that in place. I tried changing it to be .then(result) instead of .then(result=> but that broke it, so i guess that wasn’t right…

Yes, my apologies as I didn’t look at the rest of the code last night. The short version is you just need to return the Promise.all().

:link: Updated Github Gist revision diff

It looks like the refresh() is treated as a Promise in the init() function. It only attempts to schedule the refresh after the refresh() is successful, but since we weren’t returning a promise, the .then() to schedule things wasn’t doing anything.

1 Like

Thanks! I’ll try that.