Fully kiosk volume slider

Now I’m at the stage the slider works, it loads up, but it doesn’t adjust the volume.
I have variables set for the part of the url before and after what the input needs to be. But I can’t figure out what to put there for the slider result.
Now when I move the slider, it sends the request, but it always sets the volume to 50 (the default value of the slider).

Are you using onchange event listener in the range element to trigger the volume setting request? If so, you should be able to get the event’s value directly. Can you post the code snippet so we can better understand and help?

Yes, using onchange, I have tried a number of things as URL 3, with varying results. Most gave me an error message from remote admin, discarding non-numeral or invalid numbers. The current code always sets volume to 50 instead of the desired number. The text beneath the bar changes correctly though, so guess I’m inputting a wrong part.

As I said, I’m completely new at this, so this might be something really stupid and I’m just trying to understand things and using snippets from various sources to try out. So it’s possible there’s stuff in there that’s completely useless.

Here’s the full code:

<!DOCTYPE html>
<html>

<body>

<h1>Custom Range Slider</h1>
<p>Drag the slider to display the current value.</p>

<div class="slidecontainer">
  <input type="range" min="0" max="100" value="50" step="10" class="slider" id="myRange" list="tickmarks">
  
  <datalist id="tickmarks">
<option value="0"></option>
<option value="10"></option>
<option value="20"></option>
<option value="30"></option>
<option value="40"></option>
<option value="50"></option>
<option value="60"></option>
<option value="70"></option>
<option value="80"></option>
<option value="90"></option>
<option value="100"></option>
</datalist>

  <p>Value: <span id="demo"></span></p>
</div>

<script>
let slider = document.getElementById("myRange");
let output = document.getElementById("demo");
let URL1 = "IP ADDRESS:2323/?cmd=setAudioVolume&level="
let URL2 = "&stream=3&password=XXX"

let URL3 = Number(myRange.value)
let URL = URL1 + URL3 + URL2

output.innerHTML = slider.value;

  slider.onchange = function() {
  output.innerHTML = this.value;
      var popupwin = window.open(URL,'Volume','width=150,height=150,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 6000);
}


</script>

</body>
</html>

I declare myself an idiot…

As in my code, all the variables are before the function, they need to be inside.
I got it working!

PS: for anyone searching for this, here’s the code. I’m still working on styling it a bit (aka learning how to). I already made it vertical, it fits better to my needs. From what I read, this also only works in Chrome.

To use, replace IP ADDRESS with your own and XXX with your remote admin password, inside the URL1 and URL2 parts of the code. To handle different audio streams, like bluetooth, calls or alarms, edit the number after “stream” in URL2.

Quick edit so the volume level is shown while moving the slider, so you can tell what volume you will be setting to.

<!DOCTYPE html>
<html>

<body>
<div class="slidecontainer">
  <input type="range" min="0" max="100" step="10" class="slider" id="myRange" list="tickmarks" orient="vertical">
<style>

.slider{
-webkit-appearance: slider-vertical;
width: 8px;
height: 225px;
padding: 0 5px;
position: relative

}

</style>
  <datalist id="tickmarks">
<option value="0"></option>
<option value="10"></option>
<option value="20"></option>
<option value="30"></option>
<option value="40"></option>
<option value="50"></option>
<option value="60"></option>
<option value="70"></option>
<option value="80"></option>
<option value="90"></option>
<option value="100"></option>
</datalist>

  <p>Volume: <span id="demo"></span></p>
</div>

<script>
let slider = document.getElementById("myRange");
let output = document.getElementById("demo");
let URL1 = "http://IP ADDRESS/?cmd=setAudioVolume&level="
let URL2 = "&stream=3&password=XXX"

output.innerHTML = slider.value;

  slider.oninput = function() {
  output.innerHTML = this.value;
}

  slider.onchange = function() {
  output.innerHTML = this.value;
  let URL3 = this.value
  let URL = URL1 + URL3 + URL2
      var popupwin = window.open(URL,'Volume','width=15,height=15,left=5,top=3');
    setTimeout(function() { popupwin.close();}, 1000);
}


</script>

</body>
</html>
3 Likes

Awesome work and thanks for sharing! You solved a need and picked up some new skills while at it! :star::muscle:

1 Like

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”:}]}}