HADashboard Custom(ize) Widget

Hello,

I configured a hadashboard for use with home hass.io setup. I have an alarm system configured, so I’ve been using the alarm widget in hadashboard. I’d like to customize the look and feel of the alarm widget. I found the appdaemon source code that includes the widgets, but anytime I try to create a custom widget, renaming the alarm widget to something custom, my dashboard acts funny (any widget in the layout that comes after the alarm widget is gray, with no text).

My question is: how do I go about overriding the default widget so that I can customize the jss/css/html?

Details:
Hass.io: 0.87.0
Appdaemon 3 (installed through the hass.io add on store)

/config/appdaemon/custom_widget:

  • created folder named alarm_cust (/config/appdaemon/custom_widget/alarm_cust/)
  • copied the alarm.yaml file into this folder and renamed it to alarm_cust.yaml (/config/appdaemon/custom_widget/alarm_cust.yaml)

Updated references to basealarm to be alarm_cust.
/config/appdaemon/dashboards/Alarm.dash:

##
## Main arguments, all optional
##
title: Alarm Panel
skin: zen
widget_dimensions: [100, 100]
widget_margins: [5, 5]
widget_size: [1, 1]
columns: 5
global_parameters:
    use_comma: 0
    precision: 1
    use_hass_icon: 1

home_label:
    widget_type: label
    title: Home
    widget_style: "background: transparent; vertical-align: top; padding-right: 10px"
    title_style: "text-align: left; font-size: 42px; vertical-align: top; margin-top: -20px"

clock:
    widget_type: clock
    title_style: "text-align: right; font-size: 42px; vertical-align: top; margin-top: -50px"


### Alarm ###
alarm_control_panel:
    entity: alarm_control_panel.home_alarm
    widget_type: alarm
    title: Alarm Keypad
    widget_style: "background-color: #fff;"

garage_door_sensor:
  widget_type: binary_sensor
  entity: binary_sensor.garage
  title: Garage Door
  icon_on: mdi-locker
  icon_off: mdi-locker

slider_door_sensor:
  widget_type: binary_sensor
  entity: binary_sensor.slider
  title: Slider
  icon_on: mdi-locker
  icon_off: mdi-locker

front_door_sensor:
  widget_type: binary_sensor
  entity: binary_sensor.front
  title: Front Door
  icon_on: mdi-locker
  icon_off: mdi-locker

livingroom_motion:
    widget_type: binary_sensor
    title: Living Room 
    icon_on: mdi-eye-outline
    icon_off: mdi-eye-off-outline
    entity: binary_sensor.living_room

diningroom_motion:
    widget_type: binary_sensor
    title: Dining Room 
    icon_on: mdi-eye-outline
    icon_off: mdi-eye-off-outline
    entity: binary_sensor.dining_room

### Climate ###
downstairs_thermostat:
  widget_type: climate
  entity: climate.downstairs
  title: Downstairs
  units: "°F"

upstairs_thermostat:
  widget_type: climate
  entity: climate.upstairs
  title: Upstairs 
  units: "°F"

### Lighting ###
living_room_lights:
    entity: light.honeywell_39351__zw3005_inwall_smart_dimmer_level
    title: Living Room
    widget_type: input_slider

layout:
    - clock.clock(2x1), alarm_control_panel
    - garage_door_sensor,  front_door_sensor, slider_door_sensor, livingroom_motion, diningroom_motion
    - downstairs_thermostat, upstairs_thermostat

first i must say that you need to provide yaml as codeblock. this is unreadable.

as soon as you create a custom widget that is broken, the dashboard trying to use that broken widget will be broken.

the directory to place custom widgets is called custom_widgets and not custom_widget

if you want to create a custom widget with another name (which is the best way) you also need to change all references inside the HTML, JS, CSS and yaml files

so in your case you need to look through all those files and everywhere it is replace alarm with alarm_cust.

the docs about this can be found here
https://appdaemon.readthedocs.io/en/latest/WIDGETDEV.html

Thanks, Rene. I fixed my original post - apologies for the poor formatting. Also, thanks for all the work you do and all the replies on the forums; I’ve seen you helping a lot, and we all appreciate it.

I started over with a fresh copy of basealarm (grabbed from gitrepo at: https://github.com/home-assistant/appdaemon.git).

I copied the basealarm folder into /config/appdaemon/custom_widgets, and renamed it to alarm_cust.
I copied the alarm.yaml file into /config/appdaemon/custom_widgets folder and renamed it to alarm_cust.
I then renamed all of the files from basealarm.ext to alarm_cust.ext and did a find/replace on all instances of basealarm, changing it to alarm_cust. In my dashboard, I changed the widget_type from alarm to alarm_cust.

Aside from find/replace and renaming the files, I haven’t done anything else to the files - just trying to get to the point where it is working properly before I modify the code.

Code:
/config/appdaemon/dashboards/Alarm.dash:

##
## Main arguments, all optional
##
title: Alarm Panel
skin: zen
widget_dimensions: [120, 120]
widget_margins: [5, 5]
widget_size: [1, 1]
columns: 5
global_parameters:
    use_comma: 0
    precision: 1
    use_hass_icon: 1

home_label:
    widget_type: label
    title: Home
    widget_style: "background: transparent; vertical-align: top; padding-right: 10px"
    title_style: "text-align: left; font-size: 42px; vertical-align: top; margin-top: -20px"

clock:
    widget_type: clock
    title_style: "text-align: right; font-size: 42px; vertical-align: top; margin-top: -50px"


### Alarm ###
alarm_control_panel:
    entity: alarm_control_panel.home_alarm
    widget_type: alarm_cust
    title: Alarm Keypad
    widget_style: "background-color: #fff;"

garage_door_sensor:
  widget_type: binary_sensor
  entity: binary_sensor.garage
  title: Garage Door
  icon_on: mdi-locker
  icon_off: mdi-locker

slider_door_sensor:
  widget_type: binary_sensor
  entity: binary_sensor.slider
  title: Slider
  icon_on: mdi-locker
  icon_off: mdi-locker

front_door_sensor:
  widget_type: binary_sensor
  entity: binary_sensor.front
  title: Front Door
  icon_on: mdi-locker
  icon_off: mdi-locker

livingroom_motion:
    widget_type: binary_sensor
    title: Living Room 
    icon_on: mdi-eye-outline
    icon_off: mdi-eye-off-outline
    entity: binary_sensor.living_room

diningroom_motion:
    widget_type: binary_sensor
    title: Dining Room 
    icon_on: mdi-eye-outline
    icon_off: mdi-eye-off-outline
    entity: binary_sensor.dining_room

### Climate ###
downstairs_thermostat:
  widget_type: climate
  entity: climate.downstairs
  title: Downstairs
  units: "°F"

upstairs_thermostat:
  widget_type: climate
  entity: climate.upstairs
  title: Upstairs 
  units: "°F"

### Lighting ###
living_room_lights:
    entity: light.honeywell_39351__zw3005_inwall_smart_dimmer_level
    title: Living Room
    widget_type: input_slider

layout:
    - clock.clock(2x1), alarm_control_panel
    - garage_door_sensor,  front_door_sensor, slider_door_sensor, livingroom_motion, diningroom_motion
    - downstairs_thermostat, upstairs_thermostat

/config/appdaemon/custom_widgets/alarm_cust.yaml:

widget_type: alarm_cust
entity: "{{entity}}"
initial_string: "Enter Code"
post_service_ah:
    service: alarm_control_panel/alarm_arm_home
    entity_id: "{{entity}}"
post_service_aa:
    service: alarm_control_panel/alarm_arm_away
    entity_id: "{{entity}}"
post_service_da:
    service: alarm_control_panel/alarm_disarm
    entity_id: "{{entity}}"
post_service_tr:
    service: alarm_control_panel/alarm_trigger
    entity_id: "{{entity}}"
state_map:
  pending: Pending
  armed_home: Armed Home
  armed_away: Armed Away
  disarmed: Disarmed
  triggered: Triggered
fields:
  title: "{{title}}"
  title2: "{{title2}}"
  state: ""
  code: ""
static_css:
  title_style: $alarm_title_style
  title2_style: $alarm_title2_style
  state_style: $alarm_state_style
  widget_style: $alarm_widget_style
  panel_state_style: $alarm_panel_state_style
  panel_code_style: $alarm_panel_code_style
  panel_background_style: $alarm_panel_background_style
  panel_button_style: $alarm_panel_button_style
css: []
icons: []
static_icons: []

/config/appdaemon/custom_widgets/alarm_cust.css

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .state {
	display: inline-block;
	vertical-align: middle;
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .title {
	position: absolute;
	top: 5px;
	width: 100%;
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .title2 {
	position: absolute;
	top: 23px;
	width: 100%;
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .code {
	width: 100%;
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .toggle-area {
	z-index: 10;
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .container {
    width: 275px;
    display: inline-block;
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .panel-state {
    font-size: 100%;
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .block {
    display: inline-block;
    width: 75px;
    height: 75px;
    margin: 5px;
    float: top;
    font-size: 175%;
    text-align: center;
    vertical-align: middle;
    line-height: 75px; 
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .block2 {
    width: 165px;
}

/*noinspection ALL*/
.widget-alarm_cust-{{id}}  .block3 {
    width: 255px;
}

/config/appdaemon/custom_widgets/alarm_cust/alarm_cust.html

<span class="toggle-area" id="switch"></span>
<h1 class="title" data-bind="text: title, attr:{ style: title_style}"></h1>
<h1 class="title2" data-bind="text: title2, attr:{ style: title2_style}"></h1>
<h2 class="value" data-bind="text: state, attr:{ style: state_style}"></h2>
<div id="Dialog" class="modalDialog">
<div data-bind="attr:{style: panel_background_style}">
<h2 id="close" class="modalDialogCloseButton">X</h2>
<h2 class="panel-state" data-bind="text: state, attr:{style: panel_state_style}"></h2>
<h2 class="panel-state" data-bind="text: code, attr:{style: panel_code_style}"></h2>
<div class="container">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="1">1</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="2">2</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="3">3</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="4">4</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="5">5</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="6">6</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="7">7</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="8">8</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block" id="9">9</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block block2" id="0">0</span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block mdi mdi-keyboard-backspace" id="BS"></span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block mdi mdi-home" id="AH" ></span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block mdi mdi-home-outline" id="AA"></span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block fa fa-power-off" id="DA"></span class="block">
    <span data-bind="attr:{style: panel_button_style}" class="block block3" id="TR">Trigger</span class="block">
</div>
</div>
</div>

/config/appdaemon/custom_widgets/alarm_cust/alarm_cust.js

function alarm_cust(widget_id, url, skin, parameters)
{
    // Will be using "self" throughout for the various flavors of "this"
    // so for consistency ...
    
    self = this
    
    // Initialization
    
    self.widget_id = widget_id

    // Parameters may come in useful later on
    
    self.parameters = parameters
       
    self.OnButtonClick = OnButtonClick
    self.OnCloseClick = OnCloseClick
    self.OnDigitClick = OnDigitClick
    self.OnArmHomeClick = OnArmHomeClick
    self.OnArmAwayClick = OnArmAwayClick
    self.OnDisarmClick = OnDisarmClick
    self.OnTriggerClick = OnTriggerClick
    
    
    var callbacks =
        [
            {"selector": '#' + widget_id + ' > span', "action": "click", "callback": self.OnButtonClick},
            {"selector": '#' + widget_id + ' #close', "action": "click", "callback": self.OnCloseClick},
            {"selector": '#' + widget_id + ' #0', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "0"}},
            {"selector": '#' + widget_id + ' #1', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "1"}},
            {"selector": '#' + widget_id + ' #2', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "2"}},
            {"selector": '#' + widget_id + ' #3', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "3"}},
            {"selector": '#' + widget_id + ' #4', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "4"}},
            {"selector": '#' + widget_id + ' #5', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "5"}},
            {"selector": '#' + widget_id + ' #6', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "6"}},
            {"selector": '#' + widget_id + ' #7', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "7"}},
            {"selector": '#' + widget_id + ' #8', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "8"}},
            {"selector": '#' + widget_id + ' #9', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "9"}},
            {"selector": '#' + widget_id + ' #BS', "action": "click", "callback": self.OnDigitClick, "parameters": {"digit" : "BS"}},
            {"selector": '#' + widget_id + ' #AH', "action": "click", "callback": self.OnArmHomeClick},
            {"selector": '#' + widget_id + ' #AA', "action": "click", "callback": self.OnArmAwayClick},
            {"selector": '#' + widget_id + ' #DA', "action": "click", "callback": self.OnDisarmClick},
            {"selector": '#' + widget_id + ' #TR', "action": "click", "callback": self.OnTriggerClick},
                
        ]
 
    // 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
    
    if ("entity" in parameters)
    {
        var monitored_entities = 
            [
                {"entity": parameters.entity, "initial": self.OnStateAvailable, "update": self.OnStateUpdate}
            ]
    }
    else
    {
        var monitored_entities =  []
    }
    // Finally, call the parent constructor to get things moving
    
    WidgetBase.call(self, widget_id, url, skin, parameters, monitored_entities, callbacks)  

    self.set_view = set_view
    
    // Function Definitions
    
    // The StateAvailable function will be called when 
    // self.state[<entity>] has valid information for the requested entity
    // state is the initial state
    // Methods

    function OnStateAvailable(self, state)
    {    
        self.set_field(self, "state", self.map_state(self, state.state))
    }
 
    function OnStateUpdate(self, state)
    {
        self.set_field(self, "state", self.map_state(self, state.state))
    }  

    function OnButtonClick(self)
    {
        self.code = self.parameters.initial_string
        self.set_view(self)

        $('#' + widget_id + ' > #Dialog').removeClass("modalDialogClose")
        $('#' + widget_id + ' > #Dialog').addClass("modalDialogOpen")
    }

    function OnCloseClick(self)
    {
        $('#' + widget_id + ' > #Dialog').removeClass("modalDialogOpen")
        $('#' + widget_id + ' > #Dialog').addClass("modalDialogClose")
    }

    function OnDigitClick(self, parameters)
    {
        if (parameters.digit == "BS")
        {
            if (self.code != self.parameters.initial_string)
            {
                if (self.code.length == 1)
                {
                    self.code = self.parameters.initial_string
                }
                else
                {
                    self.code = self.code.substring(0, self.code.length - 1);
                }
            }
        }
        else
        {
            if (self.code == self.parameters.initial_string)
            {
                self.code = parameters.digit
            }
            else
            {
                self.code = self.code + parameters.digit
            }
        }
        self.set_view(self)
    }
    
    function OnArmHomeClick(self)
    {
        
        args = self.parameters.post_service_ah
        args["code"] = self.code
        self.call_service(self, args)
        
        self.code = self.parameters.initial_string
        self.set_view(self)
    }
    
    function OnArmAwayClick(self)
    {
        args = self.parameters.post_service_aa
        args["code"] = self.code
        self.call_service(self, args)
        
        self.code = self.parameters.initial_string
        self.set_view(self)
    }
    
    function OnDisarmClick(self)
    {
        args = self.parameters.post_service_da
        args["code"] = self.code
        self.call_service(self, args)
        
        self.code = self.parameters.initial_string
        self.set_view(self)
    }
    
    function OnTriggerClick(self)
    {
        args = self.parameters.post_service_tr
        args["code"] = self.code
        self.call_service(self, args)

        self.code = self.parameters.initial_string        
        self.set_view(self)
    }
    
    function set_view(self)
    {
        self.set_field(self, "code", self.code)
    }
}

Screenshots:
Before copying basealarm into custom_widgets and renaming/updating the files:

After copying basealarm into custom_widgets and renaming/updating the files:

i dont know if it was a wise move to call everything alarm_cust.
i think there is a reason why the derived widget and the base widget have different names.
so i would change the name from the yaml file and use that name in your dashboard.

also make sure that the filerights are right when copying files. (appdaemon needs read write access to all files in the config)

that said, you can start debugging.
2 places where you can find errors:

  1. in the browser. in google chrome you can find the console by using CTRL SHIFT I, look which errors you got there and what it tells you.
  2. in appdaemon.yaml you can set a logfile for dashboard access in the section log add
    accessfile: /the/path/to/dashboard.log
    dont forget to restart AD after that. now you get way more info about every moment that you connect, load or recompile a dashboard.

i think it must be the fact that your derived widget is the same name as your base widget, because i dont see anything wrong with the files.

and you are welcome for the help. i am glad you appreciate it, thats always nice to hear.

Just following up - I was able to get everything working. Your tip regarding enabling logging in AppDaemon led me to reread the documentation on configuring it within HA.

Your tip on inspecting the elements, brought to my attention that the old javascript from the original widget was being loaded. I set appdaemon to force recompilation (dash_force_compile: 1) on every load of the dashboard, which finally forced AppDaemon to recognize that I had created a “custom” widget. From there, I was able to make all of the changes I wanted and now everything is working as expected.

I’m still tinkering, but once I’m done, I’ll disable the dash_force_compile.

Thanks again for the help, Rene.

1 Like