Hi,
I’m having some trouble with my custom widget.
I’m building this for Sonos but it might work as well with other players.
The widget works fine except for one strange bug:
Sonos starts playing when you load the dashboard. (SOLVED)
I could really use some help with this javascript.
custom_widgets/sonos.yaml
widget_type: basesonos
entity: {{entity}}
post_service_next:
service: media_player/media_next_track
entity_id: {{entity}}
post_service_previous:
service: media_player/media_previous_track
entity_id: {{entity}}
post_service_play_pause:
service: media_player/media_play_pause
entity_id: {{entity}}
post_service_pause:
service: media_player/media_pause
entity_id: {{entity}}
post_service_stop:
service: media_player/media_stop
entity_id: {{entity}}
post_service_level:
service: media_player/volume_set
entity_id: {{entity}}
post_service:
service: media_player/select_source
entity_id: {{entity}}
fields:
title: {{title}}
artist: ""
media_title: ""
album: ""
play_icon_style: ""
pause_icon_style: ""
previous_icon_style: ""
next_icon_style: ""
state_text: ""
level: ""
inputoptions: []
selectedoption: ""
icons:
play_icon: $media_player_icon_play
pause_icon: $media_player_icon_pause
static_icons:
previous_icon: $media_player_icon_previous
next_icon: $media_player_icon_next
icon_up: $media_player_icon_up
icon_down: $media_player_icon_down
static_css:
previous_icon_style: $media_player_icon_style_previous
next_icon_style: $media_player_icon_style_next
title_style: $media_player_title_style
artist_style: $media_player_artist_style
album_style: $media_player_album_style
media_title_style: $media_player_media_title_style
state_text_style: $media_player_state_text_style
level_style: $media_player_level_style
level_up_style: $media_player_level_up_style
level_down_style: $media_player_level_down_style
widget_style: $media_player_widget_style
units_style: $media_player_units_style
select_style: $media_select_select_style
selectcontainer_style: $media_select_container_style
css:
icon_style_active: $media_player_icon_style_active
icon_style_inactive: $media_player_icon_style_inactive
custom_widgets/basesonos/basesonos.yaml
widget_type: basesonos
entity: {{entity}}
post_service_next:
service: media_player/media_next_track
entity_id: {{entity}}
post_service_previous:
service: media_player/media_previous_track
entity_id: {{entity}}
post_service_play_pause:
service: media_player/media_play_pause
entity_id: {{entity}}
post_service_pause:
service: media_player/media_pause
entity_id: {{entity}}
post_service_stop:
service: media_player/media_stop
entity_id: {{entity}}
post_service_level:
service: media_player/volume_set
entity_id: {{entity}}
post_service:
service: media_player/select_source
entity_id: {{entity}}
fields:
title: {{title}}
artist: ""
media_title: ""
album: ""
play_icon_style: ""
pause_icon_style: ""
previous_icon_style: ""
next_icon_style: ""
state_text: ""
level: ""
inputoptions: []
selectedoption: ""
icons:
play_icon: $media_player_icon_play
pause_icon: $media_player_icon_pause
static_icons:
previous_icon: $media_player_icon_previous
next_icon: $media_player_icon_next
icon_up: $media_player_icon_up
icon_down: $media_player_icon_down
static_css:
previous_icon_style: $media_player_icon_style_previous
next_icon_style: $media_player_icon_style_next
title_style: $media_player_title_style
artist_style: $media_player_artist_style
album_style: $media_player_album_style
media_title_style: $media_player_media_title_style
state_text_style: $media_player_state_text_style
level_style: $media_player_level_style
level_up_style: $media_player_level_up_style
level_down_style: $media_player_level_down_style
widget_style: $media_player_widget_style
units_style: $media_player_units_style
select_style: $media_select_select_style
selectcontainer_style: $media_select_container_style
css:
icon_style_active: $media_player_icon_style_active
icon_style_inactive: $media_player_icon_style_inactive
custom_widgets/basesonos/basesonos.html
<h1 class="title" data-bind="text: title, attr:{style: title_style}"></h1>
<div class="summary">
<div class="valign">
<p class="artist" data-bind="text: artist, attr:{style: artist_style}" />
<p class="media_title" data-bind="text: media_title, attr:{style: media_title_style}"/>
<p class="album" data-bind="text: album, attr:{style: album_style}"/>
</div>
</div>
<h2 id="previous" class="previous" data-bind="attr:{style: previous_icon_style}"><i data-bind="attr: {class: previous_icon}"></i></h2>
<h2 id="play" class="play" data-bind="attr:{style: play_icon_style}"><i data-bind="attr: {class: play_icon}"></i></h2>
<h2 id="next" class="next" data-bind="attr:{style: next_icon_style}"><i data-bind="attr: {class: next_icon}"></i></h2>
<p class="state_text" data-bind="text: state_text, attr:{style: state_text_style}"></p>
<div class="levelunit">
<p class="level" data-bind="text: level, attr:{style: level_style}"></p>
<p class="unit" data-bind="attr:{style: units_style}">%</p>
</div>
<p class="secondary-icon minus"><i data-bind="attr: {class: icon_down, style: level_down_style}" id="level-down"></i></p>
<p class="secondary-icon plus"><i data-bind="attr: {class: icon_up, style: level_up_style}" id="level-up"></i></p>
<div class="styled-select" data-bind="attr:{style: selectcontainer_style}">
<select data-bind="options: inputoptions, value: selectedoption, attr:{style: select_style}"></select>
</div>
custom_widgets/basesonos/basesonos.css
.widget-basesonos-{{id}} {
position: relative;
}
.widget-basesonos-{{id}} .title {
position: absolute;
top: 5px;
width: 100%;
color: gray;
}
.widget-basesonos-{{id}} .summary {
margin-top: -100px;
width: 100%;
height: 90px;
text-align: center;
display: table;
table-layout: fixed;
overflow: hidden;
}
.widget-basesonos-{{id}} .valign {
display: table-cell;
vertical-align: middle;
width: 100%;
}
.widget-basesonos-{{id}} .artist {
color: gray;
font-size: 90%;
font-weight: bold;
margin: 5px;
}
.widget-basesonos-{{id}} .media_title {
font-size: 90%;
font-weight: bold;
color: gray;
margin: 5px;
}
.widget-basesonos-{{id}} .album {
font-size: 80%;
color: gray;
margin: 5px;
}
.widget-basesonos-{{id}} .state_text {
position: absolute;
top: 28px;
width: 100%;
}
.widget-basesonos-{{id}} .previous {
position: absolute;
top: 110px;
left: 25px;
z-index: 10;
}
.widget-basesonos-{{id}} .play {
position: absolute;
top: 110px;
width: 100%;
}
.widget-basesonos-{{id}} .next {
position: absolute;
top: 110px;
right: 25px;
z-index: 10;
}
.widget-basesonos-{{id}} .level {
display: inline-block;
}
.widget-basesonos-{{id}} .unit {
display: inline-block;
}
.widget-basesonos-{{id}} .levelunit {
position: absolute;
top: 173px;
width: 100%;
}
.widget-basesonos-{{id}} .secondary-icon {
position: absolute;
bottom: 0px;
font-size: 20px;
width: 32px;
color: white;
}
.widget-basesonos-{{id}} .secondary-icon.plus {
right: 40px;
top: 165px;
}
.widget-basesonos-{{id}} .secondary-icon.plus i {
padding-top: 10px;
padding-left: 10px;
font-size: 150%;
}
.widget-basesonos-{{id}} .secondary-icon.minus {
left: 33px;
top: 165px;
}
.widget-basesonos-{{id}} .secondary-icon.minus i {
padding-top: 10px;
padding-right: 30px;
font-size: 150%;
}
.widget-basesonos-{{id}} .styled-select {
position: absolute;
top: 205px;
height: 29px;
overflow: hidden;
left: 27px;
width: 188px;
horizontal-align: center;
margin:auto;
}
.widget-basesonos-{{id}} .styled-select>select {
background-color: white;
border: gray solid 3px;
outline: none;
font-size: 12pt;
height: 29px;
padding: 1px;
width: 100%;
color: gray;
-webkit-border-radius: 14px;
-moz-border-radius: 14px;
border-radius: 14px;
}
custom_widgets/basesonos/basesonos.js
function basesonos(widget_id, url, skin, parameters)
{
self = this;
initial_load = true;
source_list = ["[Favorites]"]; // First value in the array
// Initialization
self.widget_id = widget_id;
// Parameters may come in useful later on
self.parameters = parameters;
self.onClick = onClick;
self.OnPlayButtonClick = OnPlayButtonClick;
self.OnPreviousButtonClick = OnPreviousButtonClick;
self.OnNextButtonClick = OnNextButtonClick;
self.OnRaiseLevelClick = OnRaiseLevelClick;
self.OnLowerLevelClick = OnLowerLevelClick;
self.min_level = 0;
self.max_level = 1;
if ("step" in self.parameters)
{
self.step = self.parameters.step / 100;
}
else
{
self.step = 0.1;
}
var callbacks =
[
{"selector": '#' + widget_id + ' #play', "action": "click", "callback": self.OnPlayButtonClick},
{"selector": '#' + widget_id + ' #level-up', "action": "click", "callback": self.OnRaiseLevelClick},
{"selector": '#' + widget_id + ' #level-down', "action": "click", "callback": self.OnLowerLevelClick},
{"selector": '#' + widget_id + ' #previous', "action": "click", "callback": self.OnPreviousButtonClick},
{"selector": '#' + widget_id + ' #next', "action": "click", "callback": self.OnNextButtonClick},
{"selector": '#' + widget_id + ' > div > select', "action": "change", "callback": self.onClick}
];
// Define callbacks for entities - this model allows a widget to monitor multiple entities if needed
// Initial will be called when the dashboard loads and state has been gathered for the entity
// Update will be called every time an update occurs for that entity
self.OnStateAvailable = OnStateAvailable;
self.OnStateUpdate = OnStateUpdate;
var monitored_entities =
[
{"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate}
];
// Finally, call the parent constructor to get things moving
WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks);
// Function Definitions
// The StateAvailable function will be called when
// self.state[<entity>] has valid information for the requested entity
// state is the initial state
function OnStateAvailable(self, state)
{
self.state = state.state;
source_list = source_list.concat(state.attributes.source_list);
set_options(self, source_list, state);
// set_value(self, state);
self.entity = state.entity_id;
self.level = state.attributes.volume_level;
set_view(self, state)
if ("dump_capabilities" in self.parameters && self.parameters["dump_capabilities"] == "1")
{
display_supported_functions(self)
}
}
// The OnStateUpdate function will be called when the specific entity
// receives a state update - its new values will be available
// in self.state[<entity>] and returned in the state parameter
function OnStateUpdate(self, state)
{
self.level = state.attributes.volume_level;
set_view(self, state);
}
function OnPlayButtonClick(self)
{
if (self.entity_state[self.entity].state !== "playing")
{
if (is_supported(self, "PLAY_MEDIA"))
{
args = self.parameters.post_service_play_pause;
self.call_service(self, args);
}
else
{
console.log("Play attribute not supported")
}
}
else
{
if (is_supported(self, "PAUSE"))
{
args = self.parameters.post_service_pause;
self.call_service(self, args)
}
else if (is_supported(self, "STOP"))
{
args = self.parameters.post_service_stop;
self.call_service(self, args)
}
else if (is_supported(self, "STOP"))
{
args = self.parameters.post_service_stop;
self.call_service(self, args)
}
else
{
// Try Play/Pause
args = self.parameters.post_service_play_pause;
self.call_service(self, args)
}
}
}
function OnPreviousButtonClick(self)
{
if (is_supported(self, "PREVIOUS_TRACK"))
{
args = self.parameters.post_service_previous;
self.call_service(self, args)
}
else
{
console.log("NEXT_TRACK attribute not supported")
}
}
function OnNextButtonClick(self)
{
if (is_supported(self, "NEXT_TRACK"))
{
args = self.parameters.post_service_next;
self.call_service(self, args)
}
else
{
console.log("NEXT_TRACK attribute not supported")
}
}
function OnRaiseLevelClick(self)
{
self.level = Math.round((self.level + self.step) * 100) / 100;
if (self.level > self.max_level)
{
self.level = self.max_level
}
args = self.parameters.post_service_level;
args["volume_level"] = self.level;
self.call_service(self, args)
}
function OnLowerLevelClick(self)
{
self.level = Math.round((self.level - self.step) * 100) / 100;
if (self.level < self.min_level)
{
self.level = self.min_level
}
args = self.parameters.post_service_level;
args["volume_level"] = self.level;
self.call_service(self, args)
}
function set_view(self, state)
{
self.set_field(self, "artist", " ");
self.set_field(self, "media_title", " ");
self.set_field(self, "album", " ");
if (state.state === "playing")
{
self.set_field(self, "play_icon_style", self.css.icon_style_active)
self.set_icon(self, "play_icon", self.icons.pause_icon)
}
else
{
self.set_field(self, "play_icon_style", self.css.icon_style_inactive)
self.set_icon(self, "play_icon", self.icons.play_icon)
}
if ("media_artist" in state.attributes)
{
self.set_field(self, "artist", state.attributes.media_artist);
}
if ("media_album_name" in state.attributes)
{
self.set_field(self, "album", state.attributes.media_album_name)
}
else
{
self.set_field(self, "album", " ");
}
if ("media_title" in state.attributes)
{
if ("truncate_name" in self.parameters)
{
name = state.attributes.media_title.substring(0, self.parameters.truncate_name);
}
else
{
name = state.attributes.media_title
}
self.set_field(self, "media_title", name);
}
if ("volume_level" in state.attributes)
{
self.set_field(self, "level", Math.round(state.attributes.volume_level * 100))
}
else
{
self.set_field(self, "level", 0)
}
}
function is_supported(self, attr)
{
var support =
{
"PAUSE": 1,
"SEEK": 2,
"VOLUME_SET": 4,
"VOLUME_MUTE": 8,
"PREVIOUS_TRACK": 16,
"NEXT_TRACK": 32,
"TURN_ON": 128,
"TURN_OFF": 256,
"PLAY_MEDIA": 512,
"VOLUME_STEP": 1024,
"SELECT_SOURCE": 2048,
"STOP": 4096,
"CLEAR_PLAYLIST": 8192,
"PLAY": 16384,
"SHUFFLE_SET": 32768
};
var supported = self.entity_state[parameters.entity].attributes.supported_features;
if (attr in support)
{
var attr_value = support[attr];
if ((supported & attr_value) == attr_value)
{
return true
}
else
{
return false
}
}
else
{
console.log("Unknown media player attribute: " + attr)
return false
}
}
function display_supported_functions(self)
{
console.log(self.parameters.entity);
console.log("Supported Features: " + self.entity_state[parameters.entity].attributes.supported_features);
console.log("PAUSE: " + is_supported(self, "PAUSE"))
console.log("SEEK: " + is_supported(self, "SEEK"))
console.log("VOLUME_SET: " + is_supported(self, "VOLUME_SET"))
console.log("VOLUME_MUTE: " + is_supported(self, "VOLUME_MUTE"))
console.log("PREVIOUS_TRACK: " + is_supported(self, "PREVIOUS_TRACK"))
console.log("NEXT_TRACK: " + is_supported(self, "NEXT_TRACK"))
console.log("TURN_ON: " + is_supported(self, "TURN_ON"))
console.log("TURN_OFF: " + is_supported(self, "TURN_OFF"))
console.log("PLAY_MEDIA: " + is_supported(self, "PLAY_MEDIA"))
console.log("VOLUME_STEP: " + is_supported(self, "VOLUME_STEP"))
console.log("SELECT_SOURCE: " + is_supported(self, "SELECT_SOURCE"))
console.log("STOP: " + is_supported(self, "STOP"))
console.log("CLEAR_PLAYLIST: " + is_supported(self, "CLEAR_PLAYLIST"))
console.log("PLAY: " + is_supported(self, "PLAY"))
console.log("SHUFFLE_SET: " + is_supported(self, "SHUFFLE_SET"))
}
function onClick(self, state)
{
// Ignore page load, only trigger on user clicks
if (initial_load == false)
{
setTimeout(function()
{
if (self.state != self.ViewModel.selectedoption())
{
self.source = self.ViewModel.selectedoption()
args = self.parameters.post_service
args["source"] = self.source
self.call_service(self, args);
}
},500)
}
initial_load = false;
}
function set_options(self, options, state)
{
self.set_field(self, "inputoptions", options);
}
}
sonos_livingroom:
widget_type: sonos
entity: media_player.livingroom
title: "Sonos livingroom"
step: 5
#truncate_name: 28
icon_style_active: "color: #CADA2A;"
icon_style_inactive: "color: gray;"
icon_down: mdi-volume-medium
icon_up: mdi-volume-high
pause_icon: mdi-pause-circle-outline
play_icon: mdi-play-circle-outline
next_icon: mdi-skip-next-circle-outline
previous_icon: mdi-skip-previous-circle-outline
level_up_style: "color: gray;"
level_down_style: "color: gray;"
level_style: "color: gray;"
units_style: "color: gray;"
widget_style: "background: white;"
Chrome (doesn’t show the rounded corners for the dropdown)
Firefox