Fully kiosk volume slider

Per your question about how to style content in custom tiles to fit different sized devices… and my response around the complexity of styling standard HTML range inputs especially when vertical and dynamic scaling is desired… and how using an external range/slider library can sometimes simplify this, here’s an updated copy of your custom tile using the popular noUiSlider library:

<!-- Do not edit below -->
<script type="application/json" id="tile-settings">
{"schema": "0.1.0", "settings": [], "dimensions": {"height": 3, "width": 1}}
</script>
<!-- Do not edit above -->

<link href="https://unpkg.com/nouislider@15.5.1/dist/nouislider.css" rel="stylesheet">
<script src="https://unpkg.com/nouislider@15.5.1/dist/nouislider.min.js"></script>

<style>
  html, body {
    height: 100vh;
    margin: 0;
  }
  .slide-container {
    display: flex;
  }
  #range {
    height: 76vh;
    margin: 2vh 0;
    /* center it */
    margin-left: auto;
    margin-right: auto;
    padding: 2vh 2vw;
  }
  .label { 
    text-align: center;
    font-size: min(10vh, 20vw);
    line-height: 1;
    height: 20vh;
    display: flex;
    align-items: center; /* vertical center */
    justify-content: center; /* horizontal center */
  }
  .label > * {
    margin: 2vh 2vw;
  }
</style>

<div class="slidecontainer">
  <div id="range"></div>

  <div class="label">
    <span>
      Volume: <span id="volume"></span>
    </span>
  </div>
</div>

<script>
let slider = document.getElementById("range");
let output = document.getElementById("volume");
let BASE_URL = "http://IP ADDRESS/?cmd=setAudioVolume&level="
let PARAMS = "&stream=3&password=XXX"

//create the slider
noUiSlider.create(slider, {
  start: [50],
  connect: true,
  range: {
    'min': 0,
    'max': 100
  },
  step: 10,
  orientation: 'vertical',
  direction: 'rtl', //0 at bottom
  // Show a scale with the slider
  pips: {
    mode: 'steps',
    stepped: true,
    density: 1
  }
});

//anytime the value updates while dragging, update the label
range.noUiSlider.on('update', function (values, handle) {
  let value = parseInt(values[handle]); //store the value in a simplified variable name
  output.innerHTML = value; //update the label
});

//only make the API call when the value is actually changed (drag handle released)
range.noUiSlider.on('change', function(values, handle) {
  let value = parseInt(values[handle]); 
  let url = BASE_URL + value + PARAMS //craft the URL
  //make the API call by popping up the window
  var popupwin = window.open(url,'Volume','width=15,height=15,left=5,top=3');
  setTimeout(function() { popupwin.close();}, 1000);
})

//get the initial value to display
output.innerHTML = parseInt(slider.noUiSlider.get());

</script>


</div>

9w3e7Zc2xc

2 Likes

Awesome, just read the code and looks like you also fixed the issue with the label not being correct when reloading the page, if I’m not mistaken.

Looks amazing, thank you so much!

It’s still just using your 50% default. :slight_smile:
(It just reads it from the ‘range’ that gets created)

1 Like

I played with this a bit further and added some more customizations. After adding this tile to your dashboard, you can edit the tile and there’s some settings you can tweak.

By default, this version uses the native local JavaScript API that you can enable in Fully Kiosk browser (Advanced Web Settings > Enable JS API). This should result in better stability and the ability to read the current volume level when the tile is first loaded.

You can also disable that in the tile settings and you’ll be presented with options to enter the IP address and passcode for Fully Remote Admin. That approach will use your popup communication by default or you can change the tile setting to 'iframe' which is invisible and works on Fully but requires allowing mixed content in Chrome.

<!-- Do not edit below -->
<script type="application/json" id="tile-settings">
{
  "schema": "0.1.0",
  "settings": [
    {
      "type": "BOOLEAN",
      "default": true,
      "name": "useJsApi",
      "label": "Use JavaScript API?"
    },
    {
      "showIf": ["useJsApi", "==", false],
      "label": "Fully IP Address",
      "type": "STRING",
      "name": "fullyIpAddress"
    },
    {
      "name": "fullyPasscode",
      "type": "STRING",
      "showIf": ["useJsApi", "==", false],
      "label": "Fully Passcode"
    },
    {
      "name": "apiTactic",
      "type": "ENUM",
      "showIf": ["useJsApi", "==", false],
      "values": ["iframe", "popup"],
      "default": "popup",
      "label": "API Communication"
    },
    {
      "name": "audioChannel",
      "label": "Audio Channel",
      "type": "NUMBER",
      "default": 3
    }
  ],
  "name": "Fully Volume Controller",
  "dimensions": {"width": 1, "height": 3}
}
</script>
<!-- Do not edit above -->

<link href="https://unpkg.com/nouislider@15.5.1/dist/nouislider.css" rel="stylesheet">
<script src="https://unpkg.com/nouislider@15.5.1/dist/nouislider.min.js"></script>
<script src="https://cdn.sharptools.io/js/custom-tiles.js"></script>


<style>
  html, body {
    height: 100vh;
    margin: 0;
  }
  .slide-container {
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center; /* vertical align */
  }
  #range {
    height: 75vh;
    /* center it */
    margin-left: auto;
    margin-right: auto;
    margin-top: 2vh; /* move everything down a tick */
  }
  .label { 
    text-align: center;
    font-size: min(10vh, 20vw);
    line-height: 1;
    height: 18vh;
    display: flex;
    align-items: center; /* vertical center */
    justify-content: center; /* horizontal center */
    padding: 0 4vw;
  }
  iframe.api-caller {
    display: none;
  }
  iframe.api-caller {
    position: absolute;
    border: 0;
    width: 100%;
    background: rgba(255,0,0,0.5); /* red transparent for debug */
    top: 0; left: 0; right: 0; bottom: 0;
  }

  #fully-js-warning {
    background: rgba(223, 0, 0, 0.7);
    padding: 1em;
    position: fixed;
    bottom:0;
    left: 0;
    right: 0;
    max-height: calc(90% - 2em);
    overflow-y: auto;
  }
  #range[disabled] .noUi-handle, #range[disabled] .noUi-connects {
    display: none;
  }
</style>

<div class="slide-container">
  <div id="range"></div>

  <div class="label">
    <span>
      Volume: <span id="volume"></span>
    </span>
  </div>
</div>

<div id="fully-js-warning" style="display:none;">
  The Fully JavaScript API option is selected in the tile settings, but the 
  <strong>Enable JavaScript Interface</strong> setting in Fully's <strong>Advanced Web Settings</strong> 
  section is not enabled.
  <br>
  <br>
  Either enable the setting in Fully or disable the JavaScript option in this tile's settings.
</div>

<script>
let slider = document.getElementById("range");
let output = document.getElementById("volume");
let fullyJsWarning = document.getElementById("fully-js-warning");

//defaults, actuals get loaded below
let useJsApi = true; 
let fullyIpAddress = "";
let fullyPasscode = "";
let audioStream = 3;
let apiTactic = "popup";

//when the tile settings are ready, let's grab them
stio.ready(function(data){
  if(data.settings.useJsApi === false){
    useJsApi = data.settings.useJsApi;
  }
  if(data.settings.fullyIpAddress != null){
    let addr = data.settings.fullyIpAddress
    //if there's a colon, assume the user entered a port
    if(addr.indexOf(":") > -1){
    	fullyIpAddress = addr;  
    }
    else{
      fullyIpAddress = `${addr}:2323`; //add the default port
    }
    
  }
  if(data.settings.fullyIpAddress != null){
    fullyPasscode = data.settings.fullyPasscode
  }
  if(data.settings.audioStream != null){
    audioStream = parseInt(data.settings.audioStream)
  }
  if(data.settings.apiTactic != null){
    apiTactic = data.settings.apiTactic;
  }

  initVolume();
});

//create the slider
noUiSlider.create(slider, {
  start: [50],
  connect: 'lower',
  range: {
    'min': 0,
    'max': 100
  },
  step: 10,
  orientation: 'vertical',
  direction: 'rtl', //0 at bottom
  // Show a scale with the slider
  pips: {
    mode: 'steps',
    stepped: true,
    density: 1
  }
});

//anytime the value updates while dragging, update the label
range.noUiSlider.on('update', function (values, handle) {
  let value = parseInt(values[handle]); //store the value in a simplified variable name
  output.innerHTML = value; //update the label
});

//only make the API call when the value is actually changed (drag handle released)
range.noUiSlider.on('change', function(values, handle) {
  let value = parseInt(values[handle]); 
  setVolume(value);
})

function setVolume(level){
  if(useJsApi){
    void fully.setAudioVolume(level, audioStream)
  }
  else{
    let url = getCommandUrl('setAudioVolume', {level: level, stream: audioStream})
    console.debug('URL is', url);
    loadUrl(url);
  }
}

function getCommandUrl(command, params){
  let url = `http://${fullyIpAddress}/?cmd=${command}&password=${fullyPasscode}`;
  //layer in additional parameters
  Object.keys(params).forEach(key =>{
    url += `&${key}=${params[key]}`;
  })
  return url;
}

function loadUrl(url){
  if(apiTactic === "iframe"){
    //create an iframe with our class and url, then add it to the document
    let iframe = document.createElement('iframe');
    iframe.classList.add("api-caller")
    iframe.src = url;
    document.body.appendChild(iframe);
    //then remove it in a second
    setTimeout(()=>{
      document.body.removeChild(iframe);
    }, 1000)
  }
  else{
  	//make the API call by popping up the window
    var popupwin = window.open(url,'Volume','width=15,height=15,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
  }
}


function initVolume(){
  //if the tile is configured to use the JS API
  if(useJsApi){
    //if the API isn't enabled, display the warning
    if(window.fully == null){
      slider.setAttribute("disabled", true); //disable the slider;
      fullyJsWarning.style.display = "block";
    }
    else{
      //make sure the slider is enabled and the warning is not displayed
      slider.removeAttribute('disabled');
      fullyJsWarning.style.display = "none";
      //try to get the volume
      let volume = fully.getAudioVolume(audioStream)
      slider.noUiSlider.set(volume);
    }
  }

  //get the value from the slider
  output.innerHTML = parseInt(slider.noUiSlider.get());
}


</script>
1 Like

Can a slider like this be used to control Sonos speakers? I would love to be able to use this for the six sonos amps I have… it’s much more convenient and clean then up down arrows

What type of hub are you using? In theory, something like this could be used to control devices from a smart home hub, but it would need to be customized to call the relevant API.

A community member put something like this together so they could control multiple SmartThings lights at once. In theory, that could be modified to control volume instead.

There’s also a few relevant feature requests that you might want to cast a vote on (and reply with your use case to bump the thread):

1 Like

Hi Josh,
Thank you for the information. I am using Smartthings as my hub. I will take a look at a few of the other projects, however I am not very well versed in some of the more advanced coding. Thank you!

So I tried to update the light dimmer switch to work with Sonos and yea… not working, and that is because I have no clue what I am doing… am I even close? Thank you!

<!-- Do not edit above -->

<!-- Styles -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/15.5.1/nouislider.css" integrity="sha512-MKxcSu/LDtbIYHBNAWUQwfB3iVoG9xeMCm32QV5hZ/9lFaQZJVaXfz9aFa0IZExWzCpm7OWvp9zq9gVip/nLMg==" crossorigin="anonymous" referrerpolicy="no-referrer">
<style>
.main-content {
    display: flex;
    align-items: center;
    justify-content: center;
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    padding-top: 10px;
}
.slider-wrapper {
    width: 70%;
    height: 80%;
    border-radius: 8px;
}
.noUi-target {
    background-color: rgb(255 255 255 / 30%);
    border: 0;
    box-shadow: none;
    backdrop-filter: blur(8px);
    border-radius: 8px;
}
.noUi-connect {
    background: rgb(255 255 255 / 30%);
    border-radius: 8px;
}
.noUi-handle {
    opacity: 0;
    width: 90px !important;
    height: 50% !important;
    margin: 0 !important;
    right: 0 !important;
}

</style>

<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/noUiSlider/15.5.1/nouislider.min.js" integrity="sha512-T5Bneq9hePRO8JR0S/0lQ7gdW+ceLThvC80UjwkMRz+8q+4DARVZ4dqKoyENC7FcYresjfJ6ubaOgIE35irf4w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.sharptools.io/js/custom-tiles.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script>
var content = document.getElementById("mainContent");
var patToken = null;
var devices = [];
var baseUrl = "https://api.smartthings.com/v1/devices";

function initSlider() {
    slider = document.querySelector(".slider-wrapper");
    noUiSlider.create(slider, {
        start: 0,
        connect: "lower",
        direction: 'rtl',
        orientation: 'vertical',
        range: {
            'min': 0,
            'max': 100
        }
    });  
    slider.noUiSlider.on('change', function () {  
        var value = parseInt(slider.noUiSlider.get());
            console.log("Slider changed to: " + value);
            setVolLevel(value);
    });
}

function setSlider(val) {
    slider = document.querySelector(".slider-wrapper");
    slider.noUiSlider.set(val);
}

function getVolLevel() {
    var volLevel = 0;        
    axios.get(baseUrl + '/' + devices[0] + '/status', getAxiosConfig()).then(function(response) {
        console.log('Get vol level response status', response.status);
        if(response.data.components.main.volume !== undefined) {
            volLevel = response.data.components.main.volume.level.value;
            setSlider(volLevel);
        }
    });        
}
    
function setVolLevel(value) {
    var data = '{"commands": [{"component": "main", "capability": "volume", "command": "setLevel", "arguments": ['+value+']}]}';
    for(var i=0; i<devices.length; i++) {
        axios.post(baseUrl + '/' + devices[i] + '/commands', data, getAxiosConfig()).then(function(response) {
            console.log('Set vol level response status', response.status);
        });
    }
} 

function getAxiosConfig(){
    return { "headers": { "Authorization": "Bearer " + patToken } }
}
    
stio.ready((data)=>{
    console.log("Starting Volume Dimmer [shortyyy]");
    console.log("stio library is ready with token", data.settings.token);        
    if(data.settings.token == null || data.settings.DeviceID1 == null){
        console.log("Please configure the authorization token and at least 1 device");
        return;
    }
    else{            
        patToken = data.settings.token;
        for(var i=1; i<=5; i++) {
            if(data.settings["DeviceID"+i] !== null) {
                devices.push(data.settings["DeviceID"+i]);
            }
        }
        console.log("device IDs", devices);
    }

    initSlider();
    getVolLevel();
    setInterval(getVolLevel, 30000);
});


    
</script>

<!-- HTML -->
<div class="main-content" id="mainContent">
    <div class="slider-wrapper"></div>
</div>

From a quick glance, it seems like you’re headed in the right direction.

var data = '{"commands": [{"component": "main", "capability": "volume", "command": "setLevel", "arguments": ['+value+']}]}';

I would double check what your device is reporting here. You might be looking for the capability audioVolume which has a command setVolume(volume).

Hi Josh! Thank you for the pointers… I added your suggestions and nothing. I made an API token… and got the device ID off the IDE page…so I do not think that is the issue. You said to check what my device is reporting, how do I go about doing this? Thank you!!!

As a follow up… I know for other actions… like clicking a button I get a message saying command sent, however, with the slider I am not seeing this message. Is this another reason why this probably isnt working? Thanks!

Are you getting a response of 200 back?

You can verify what capabilities and commands the device is reporting in SharpTools:

  1. Open your SharpTools.io User Page
  2. Tap the ... next to your location
  3. Scroll down and tap on the device
  4. Review the capabilities section (toward the top) and commands (toward the bottom)

The Custom Tile code would have to explicitly implement that functionality which this one doesn’t.

I would recommend getting familiar with your browser’s developer tools for this kind of thing though (F12 on most modern browsers). For example, this Custom Tile logs a bunch of stuff to the console… and if there are errors, they usually show up in the console.

The json payload is wrong.
This works.

{ "commands": [ { "component": "main", "capability": "audioVolume", "command": "setVolume", "arguments": [100] } ] }
1 Like

Thank you for the help! do you have the whole script to make it work? Im still getting a lot of errors. When I do F12 I am seeing status code 400 and
:{“code”:“ConstraintViolationError”,“message”:“The request is malformed.”,“details”:[{“code”:“PatternError”,“target”:“deviceId”,“message”:“deviceId value "RINCON_5CAAFDEF097E01400:113932966" not a properly formed GUID.”,“details”:}]}}

So I am getting an error 401 when trying to access the smarrthings API… I have generated multiple tokens… they are all giving me the same issue… is there another step that I need to do to allow access to the API?

You need to replace this.

    
    var data = '{"commands": [{"component": "main", "capability": "volume", "command": "setLevel", "arguments": ['+value+']}]}';

With this. The capability and command was wrong, and the argument was being passed as a string and not an integer.

  var data = '{ "commands": [ { "component": "main", "capability": "audioVolume", "command": "setVolume", "arguments": [+value+] } ] }';

Thank you Jake… that cleaned up some of the errors. However I am still getting the error 401 please see below:

Just to confirm… I went here (Samsung account) and generated a Token… I selected every option.

I then went here (https://graph.api.smartthings.com/) to get the Device Network Id for the sonos amp I would like to control.

Is there any step that I am missing to allow this sharptools tile to communicate with the device?

I really appreciate your help, thank you!

The overall steps seem right, but something about your token apparently is not right. Did you double check that after you got the new token with DEVICE access, that you updated the settings within your Custom Tile instance that’s on your dashboard (eg. edited the settings of the tile that you put on a dashboard) and made sure to save them?

HI Josh,
Thank you for the reply. Yes I made sure to add the new token to the Custom Tile and save it. I just generated another new token, added it to the tile, saved the tile and I am still getting the same issue. I dont know enough about all of this, but seems for whatever the reason smartthings is blocking access to the device. Is there something wrong with the way the token is being used to access the API?