Show all Active Alexa Timers

Thanks @kbrown01 !
If adding an X to the existing component, I’ll need to modify the code for the card, so I’ll have another td element in each row of the table, I could easily put an X icon in it, but any idea how to do it so I could call a script from within the code of the card?

I would not do it the way it is currently so I do not know how to answer.
I would rather have a custom integration that is a sensor with all the active timers.
That way, the “X” instead of being in some table could carry the information you need to cancel it for Alexa. To cancel a specific timer, you need the name and the Alexa it is on.

By the way, I was just “mushroom-izing” that screen and I added the cancel.

image

Card code:

type: vertical-stack
cards:
  - type: custom:mushroom-select-card
    entity: input_select.alexa_devices
    icon_type: none
    primary_info: Select Alexa
    secondary_info: ''
    fill_container: true
  - type: entities
    entities:
      - entity: input_text.alexa_timer_name
        name: Timer Name
        icon: mdi:timer-check
  - type: custom:mushroom-number-card
    entity: input_number.alexa_timer_length
    icon_type: none
    layout: horizontal
    primary_info: none
    secondary_info: none
    display_mode: slider
  - type: horizontal-stack
    cards:
      - type: custom:mushroom-template-card
        entity: script.create_alexa_timer
        icon: mdi:timelapse
        icon_color: green
        primary: Create
        tap_action:
          action: call-service
          service: script.create_alexa_timer
          service_data:
            mode: create
      - type: custom:mushroom-template-card
        entity: script.create_alexa_timer
        icon: mdi:timer-cancel
        icon_color: red
        primary: Cancel
        tap_action:
          action: call-service
          service: script.create_alexa_timer
          service_data:
            mode: cancel

Script code:

alias: Create Alexa Timer
sequence:
  - service: media_player.play_media
    data_template:
      media_content_type: custom
      media_content_id: |-
        {% if mode == 'create' %} 
          set {{ states('input_text.alexa_timer_name')}} timer for {{ states('input_number.alexa_timer_length') }} minutes on {{ states('input_select.alexa_devices') }} 
        {% else %} 
          cancel {{states('input_text.alexa_timer_name') }} timer on {{states('input_select.alexa_devices') }} 
        {% endif %}
    target:
      entity_id: media_player.deck_alexa
mode: single
icon: mdi:timer-check

So if I type in “Booboo” and select “Deck Alexa” and hit cancel, it cancels the timer using media_player.play_media.

@DrMor and @kbrown01 Thanks for the feedback. I’ll take a look at it in the next week or so and either add it to my code or let you know if I can’t figure it out.

1 Like

@DrMor and @kbrown01 I played around with it for a while, and rewrote some of the code to work with this. It’s not very well styled, but it should work for you, especially since it looks like you have some knowledge of styling things. All the ‘x’ links have the CSS class “timerCancelButton”.

class CardAlexaTimers extends HTMLElement {
  // Whenever the state changes, a new `hass` object is set. Use this to
  // update your content.
  set hass(hass) {
    // Initialize the content if it's not there yet.
    this.hassStored = hass;

    if(!this.initialized) {
        // we only want to run this once
        this.initialized = true;
        this.alarms;
        this.interval = window.setInterval(this.draw.bind(this), 1000);
        this.draw();
    }

  }
  
  get hass() {
      return this.hassStored;
  }

  // The user supplied configuration. Throw an exception and Lovelace will
  // render an error card.
  setConfig(config) {
    this.config = config;
  }

  // The height of your card. Home Assistant uses this to automatically
  // distribute all cards over the available columns.
  getCardSize() {
    return 3;
  }
  draw() {
    const entities = this.config.entities;
    
    this.alarms = [];
    for(let i = 0; i < entities.length; i++) {
        if(this.hass.states[entities[i]] !== undefined) {
            const attributes = this.hass.states[entities[i]].attributes;
            if(attributes.hasOwnProperty('sorted_active')) {
                const sorted_active = JSON.parse(attributes.sorted_active);
                for(let j = 0; j < sorted_active.length; j++) {
                    const alarm = sorted_active[j][1];
                    if (alarm.triggerTime >= new Date().getTime()) {
                        this.alarms.push(new AlexaTimer(alarm.timerLabel, alarm.triggerTime, alarm.originalDurationInMillis));
                    }
                }
            }
        }
    }
    this.alarms.sort((a,b) => a.remainingTime - b.remainingTime);

    if (this.alarms.length === 0) {
        this.innerHTML = '';
        this.content = null;
    }
    
    if(this.alarms.length > 0) {
      this.innerHTML = `
        <ha-card class="alexa-timers">
          <div class="card-content"></div>
        </ha-card>
      `;
      this.content = this.querySelector('div');
      
      //let table = "<table border='0' width='100%'>";
      let table = document.createElement('table');
      table.setAttribute('border', '0');
      table.setAttribute('width', '100%');
      for(let i = 0; i < this.alarms.length; i++) {
          let name = this.alarms[i].label;
          if(name === null) {
              name = getNameFromDuration(this.alarms[i].originalDurationInMillis);
          }
          let timeLeft = this.alarms[i].hours + ":" + addLeadingZero(this.alarms[i].minutes) + ":" + addLeadingZero(this.alarms[i].seconds);
          let tr = document.createElement('tr');
          let nameTd = document.createElement('td');
          let nameTdText = document.createTextNode(name);
          nameTd.appendChild(nameTdText);
          tr.appendChild(nameTd);
          let timeLeftTd = document.createElement('td');
          let timeLeftTdText = document.createTextNode(timeLeft);
          timeLeftTd.appendChild(timeLeftTdText);
          tr.appendChild(timeLeftTd);
          let xTd = document.createElement('td');
          let a = document.createElement('a');
          let aText = document.createTextNode('X');
          a.appendChild(aText);
          a.classList.add('timerCancelButton');
          a.href = '#';
          xTd.appendChild(a);
          tr.appendChild(xTd);
          a.addEventListener('click', (e) => {
            cancelTimer(this.hass, this.config.cancel_entity, i, this.alarms);
            e.preventDefault();
          });
          table.appendChild(tr);
      }
      this.content.appendChild(table);
    }
  }
}

class AlexaTimer {
    constructor(label, triggerTime, originalDurationInMillis) {
        this.label = label;
        this.triggerTime = triggerTime;
        this.remainingTime = this.triggerTime - (new Date().getTime());
        this.seconds = Math.floor(this.remainingTime / 1000);
        this.minutes = Math.floor(this.seconds / 60);
        this.hours = Math.floor(this.minutes / 60);
        this.seconds = this.seconds % 60;
        this.minutes = this.minutes % 60;
        this.originalDurationInMillis = originalDurationInMillis;
    }
}

function addLeadingZero(num) {
    if(num < 10) {
        return "0" + num;
    }
    else {
        return num.toString();
    }
}

function getNameFromDuration(millis) {
    let seconds = Math.floor(millis / 1000);
    let minutes = Math.floor(seconds / 60);
    let hours = Math.floor(minutes / 60);
    seconds = seconds % 60;
    minutes = minutes % 60;
    let name = "";
    if(hours > 0) {
        name += hours + " Hour";
        if(minutes > 0 || seconds > 0) {
            name += ", ";
        }
        else {
            name += " ";
        }
    }
    if(minutes > 0) {
        name += minutes + " Minute";
        if(seconds > 0) {
            name += ", ";
        }
        else {
            name += " ";
        }
    }
    if(seconds > 0) {
        name += seconds + " Second ";
    }
    name += "Timer";
    return name;
}

function cancelTimer(hass, entity_id, alarm_id, alarms) {
    let timerName = "";
    if(alarms[alarm_id].label === null) {
        const filteredAlarms = alarms.filter((alarm) => alarm.originalDurationInMillis == alarms[alarm_id].originalDurationInMillis);
        timerName = getNameFromDuration(alarms[alarm_id].originalDurationInMillis);
        if(filteredAlarms.length > 1) {
            let ordinalName = "the " + getOrdinal(filteredAlarms.indexOf(alarms[alarm_id]) + 1) + " one";
            let media_content_id = "cancel " + timerName.toLowerCase();
            hass.callService("media_player", "play_media", {
                'entity_id': entity_id,
                'media_content_id': media_content_id,
                'media_content_type': "custom"
            });
            setTimeout(() => {
                hass.callService("media_player", "play_media", {
                    'entity_id': entity_id,
                    'media_content_id': ordinalName,
                    'media_content_type': "custom"
                });
            }, 2000);
        }
        else {
            let media_content_id = "cancel " + timerName.toLowerCase();
            hass.callService("media_player", "play_media", {
                'entity_id': entity_id,
                'media_content_id': media_content_id,
                'media_content_type': "custom"
            });
        }
    }
    else {
        timerName = alarms[alarm_id].label + " timer";
        let media_content_id = "cancel " + timerName.toLowerCase();
        hass.callService("media_player", "play_media", {
            'entity_id': entity_id,
            'media_content_id': media_content_id,
            'media_content_type': "custom"
        });
    }
}

function getOrdinal(num) {
    switch(num) {
        case 1:
            return "1st";
        case 2:
            return "2nd";
        case 3:
            return "3rd";
        case 4:
            return "4th";
        case 5:
            return "5th";
        case 6:
            return "6th";
        case 7:
            return "7th";
        case 8:
            return "8th";
        case 9:
            return "9th";
        case 10:
            return "10th";
        default:
            return num;
    }
}

customElements.define('card-alexa-timers', CardAlexaTimers);

You’ll also need to change the .yaml for the dashboard so it includes a new parameter: cancel_entity. That’s the name of the device that it tells to cancel the timer, and the one that responds if there is an issue. My .yaml for that section looks kind of like this for this version:

        - type: "custom:card-alexa-timers"
          entities:
            - sensor.server_rack_dot_next_timer
            - sensor.front_room_dot_next_timer
          cancel_entity: media_player.server_rack_dot

I have it set so if you try to cancel an unnamed timer that has the same duration as another one, it first tells the cancel entity to cancel the timer based on its length, as in “cancel 1 hour timer” and then waits 2 seconds and tells it “the nth one” based on remaining duration. So if you have 3 1 hour timers and click on the ‘X’ after the second one in the list, it first sends the command “cancel 1 hour timer” and waits 2 seconds and sends the command “the 2nd one”. I find that this works, although you still hear the device starting to say “You have two one hour timers…” but it gets cut off partway through and says it cancelled the timer. If I try to send it faster the device can’t handle it, and I haven’t been able to figure out how to phrase it as a single command without the initial context for the device.

Hope this works for what you need!

2 Likes

This is simply perfect. Fits my usage exactly, nothing else I would add…
here’s my layout:
Capture

For anyone interested, this is my card with CSS:

type: custom:card-alexa-timers
name: Timers
entities:
  - sensor.livingroom_echo_next_timer
cancel_entity: media_player.livingroom_echo
card_mod:
  style: |
    ha-card {
      font-size: 36px;
      line-height: 46px;
      transition: none !important;
    }  
    a:link {
      color: white;
      text-decoration: none;
    }    
    timerLeft.td
    {
      text-align: right;
    }

One really minor thing, and this is really nitpicking by now, is to replace the textual “X” with an icon, maybe this:

Or even leave it for the user the choose, but really, this is very minor, the performance is great, it works wonderfully, thank you so much for this!

Very nice! As a cook though it really needs more granular control than 5, 10, 30 and also must have naming … it is very common in the kitchen where 4 or more timers are running. WIthout names it would be impossible to remember what is what.

It should be easy to replace the with the “X” with an icon.
this is my current setup:

image

To start the timers, I prefer voice commands for Alexa, I find it easier, the 3 presets are something I’ll play around with, I definitely prefer naming them and not having generic names based on the length.

Understood and I also prefer voice. That said and even though I have 5 Alexas around, none of them are outdoors by the grill or the Caja China grill box. BUT the stereo speakers are as well as internet … so I can run a timer on a portable PAD and set it to announce on the stereo.

Life is grand!

Suppose I could install Alexa App on the PAD and speak to it …

Took me a little while of searching, but I figured out how to replace it with an icon (one of the built-in mdi icons). Here’s a new version of the code. There’s only a few lines changed, but I figured I’d paste it all rather than trying to explain what needs to be replaced.

class CardAlexaTimers extends HTMLElement {
  // Whenever the state changes, a new `hass` object is set. Use this to
  // update your content.
  set hass(hass) {
    // Initialize the content if it's not there yet.
    this.hassStored = hass;

    if(!this.initialized) {
        // we only want to run this once
        this.initialized = true;
        this.alarms;
        this.interval = window.setInterval(this.draw.bind(this), 1000);
        this.draw();
    }

  }
  
  get hass() {
      return this.hassStored;
  }

  // The user supplied configuration. Throw an exception and Lovelace will
  // render an error card.
  setConfig(config) {
    this.config = config;
  }

  // The height of your card. Home Assistant uses this to automatically
  // distribute all cards over the available columns.
  getCardSize() {
    return 3;
  }
  draw() {
    const entities = this.config.entities;
    
    this.alarms = [];
    for(let i = 0; i < entities.length; i++) {
        if(this.hass.states[entities[i]] !== undefined) {
            const attributes = this.hass.states[entities[i]].attributes;
            if(attributes.hasOwnProperty('sorted_active')) {
                const sorted_active = JSON.parse(attributes.sorted_active);
                for(let j = 0; j < sorted_active.length; j++) {
                    const alarm = sorted_active[j][1];
                    if (alarm.triggerTime >= new Date().getTime()) {
                        this.alarms.push(new AlexaTimer(alarm.timerLabel, alarm.triggerTime, alarm.originalDurationInMillis));
                    }
                }
            }
        }
    }
    this.alarms.sort((a,b) => a.remainingTime - b.remainingTime);

    if (this.alarms.length === 0) {
        this.innerHTML = '';
        this.content = null;
    }
    
    if(this.alarms.length > 0) {
      this.innerHTML = `
        <ha-card class="alexa-timers">
          <div class="card-content"></div>
        </ha-card>
      `;
      this.content = this.querySelector('div');
      
      //let table = "<table border='0' width='100%'>";
      let table = document.createElement('table');
      table.setAttribute('border', '0');
      table.setAttribute('width', '100%');
      for(let i = 0; i < this.alarms.length; i++) {
          let name = this.alarms[i].label;
          if(name === null) {
              name = getNameFromDuration(this.alarms[i].originalDurationInMillis);
          }
          let timeLeft = this.alarms[i].hours + ":" + addLeadingZero(this.alarms[i].minutes) + ":" + addLeadingZero(this.alarms[i].seconds);
          let tr = document.createElement('tr');
          let nameTd = document.createElement('td');
          let nameTdText = document.createTextNode(name);
          nameTd.appendChild(nameTdText);
          tr.appendChild(nameTd);
          let timeLeftTd = document.createElement('td');
          let timeLeftTdText = document.createTextNode(timeLeft);
          timeLeftTd.appendChild(timeLeftTdText);
          tr.appendChild(timeLeftTd);
          let xTd = document.createElement('td');
          let a = document.createElement('a');
          let aContent = document.createElement('ha-icon');
          aContent.setAttribute("icon", "mdi:close-circle-outline");
          a.appendChild(aContent);
          a.classList.add('timerCancelButton');
          a.href = '#';
          xTd.appendChild(a);
          tr.appendChild(xTd);
          a.addEventListener('click', (e) => {
            cancelTimer(this.hass, this.config.cancel_entity, i, this.alarms);
            e.preventDefault();
          });
          table.appendChild(tr);
      }
      this.content.appendChild(table);
    }
  }
}

class AlexaTimer {
    constructor(label, triggerTime, originalDurationInMillis) {
        this.label = label;
        this.triggerTime = triggerTime;
        this.remainingTime = this.triggerTime - (new Date().getTime());
        this.seconds = Math.floor(this.remainingTime / 1000);
        this.minutes = Math.floor(this.seconds / 60);
        this.hours = Math.floor(this.minutes / 60);
        this.seconds = this.seconds % 60;
        this.minutes = this.minutes % 60;
        this.originalDurationInMillis = originalDurationInMillis;
    }
}

function addLeadingZero(num) {
    if(num < 10) {
        return "0" + num;
    }
    else {
        return num.toString();
    }
}

function getNameFromDuration(millis) {
    let seconds = Math.floor(millis / 1000);
    let minutes = Math.floor(seconds / 60);
    let hours = Math.floor(minutes / 60);
    seconds = seconds % 60;
    minutes = minutes % 60;
    let name = "";
    if(hours > 0) {
        name += hours + " Hour";
        if(minutes > 0 || seconds > 0) {
            name += ", ";
        }
        else {
            name += " ";
        }
    }
    if(minutes > 0) {
        name += minutes + " Minute";
        if(seconds > 0) {
            name += ", ";
        }
        else {
            name += " ";
        }
    }
    if(seconds > 0) {
        name += seconds + " Second ";
    }
    name += "Timer";
    return name;
}

function cancelTimer(hass, entity_id, alarm_id, alarms) {
    let timerName = "";
    if(alarms[alarm_id].label === null) {
        const filteredAlarms = alarms.filter((alarm) => alarm.originalDurationInMillis == alarms[alarm_id].originalDurationInMillis);
        timerName = getNameFromDuration(alarms[alarm_id].originalDurationInMillis);
        if(filteredAlarms.length > 1) {
            let ordinalName = "the " + getOrdinal(filteredAlarms.indexOf(alarms[alarm_id]) + 1) + " one";
            let media_content_id = "cancel " + timerName.toLowerCase();
            hass.callService("media_player", "play_media", {
                'entity_id': entity_id,
                'media_content_id': media_content_id,
                'media_content_type': "custom"
            });
            setTimeout(() => {
                hass.callService("media_player", "play_media", {
                    'entity_id': entity_id,
                    'media_content_id': ordinalName,
                    'media_content_type': "custom"
                });
            }, 2000);
        }
        else {
            let media_content_id = "cancel " + timerName.toLowerCase();
            hass.callService("media_player", "play_media", {
                'entity_id': entity_id,
                'media_content_id': media_content_id,
                'media_content_type': "custom"
            });
        }
    }
    else {
        timerName = alarms[alarm_id].label + " timer";
        let media_content_id = "cancel " + timerName.toLowerCase();
        hass.callService("media_player", "play_media", {
            'entity_id': entity_id,
            'media_content_id': media_content_id,
            'media_content_type': "custom"
        });
    }
}

function getOrdinal(num) {
    switch(num) {
        case 1:
            return "1st";
        case 2:
            return "2nd";
        case 3:
            return "3rd";
        case 4:
            return "4th";
        case 5:
            return "5th";
        case 6:
            return "6th";
        case 7:
            return "7th";
        case 8:
            return "8th";
        case 9:
            return "9th";
        case 10:
            return "10th";
        default:
            return num;
    }
}

customElements.define('card-alexa-timers', CardAlexaTimers);
3 Likes

Thank you @Kethlak, you’re amazing, this is 100% perfect.

My very small contribution… :sweat_smile:, see the last 2 lines in the card_mod entry, if someone is looking to change the color and\or size of the X icon:

card_mod:
  style: |
    ha-card {
      font-size: 34px;
      line-height: 46px;
      transition: none !important;
      --mdc-icon-size: 47px;
      --card-mod-icon-color: var(--accent-color);
    }

I love this one, thanks for sharing! It would be really cool if it also had ability to display which Echo the timer was set on like this:

Hi, @jaaem’s version on github works this way. I just rewrote it a bit to include some configuration options and submitted a pull request, but you can use his code at GitHub - jdeath/card-alexa-alarms-timers: Card for Alexa Timers and Alarms or add it through HACS.

1 Like

Amazing thanks @Kethlak !! I’ve tried the new card from the Github PR but unfortunately getting this error:

Edit: Just figured it out, I didn’t have the entities_alarm: defined. It works once I add the alarm sensors :partying_face:

Thanks for the feedback! I’m going to be handling the repository from now on, and it will be at GitHub - Kethlak/card-alexa-alarms-timers: Card for Alexa Timers and Alarms . I’ve fixed it so it handles missing timers and/or missing alarms.

2 Likes

Could you give a “class” to the td elements? I’d like to align the “X” and the remainingTime to the right, while keeping the name aligned to the left. I think it can only be done in card_mod if you give them a class, if there’s a better option I’m open to suggestions… Thanks for this, it’s great to have it on HACS!

This is done on GitHub. The name tds have the class alexa-alarms-timers-name, the time tds have the class alexa-alarms-timers-time, and the cancel button tds have the class alexa-alarms-timers-x.

Forgot to @DrMor in the reply above.

Wonderful, thank you!
My code now is:

card_mod:
  style: |
    ha-card {
      font-size: 35px;
      line-height: 46px;
      transition: none !important;
      --mdc-icon-size: 47px;
      --card-mod-icon-color: var(--accent-color);
    }  
    strong 
    {
      font-size: 48px !important
    }
    td.alexa-alarms-timers-time
    {
      text-align: right;
    }
    td.alexa-alarms-timers-x
    {
      text-align: right;
    }

Thanks so much for this! Would it be possible to add a class for each timer-row or alarm-row element, for styling purposes? I’d like to target the rows so I can provide some separation with either padding or a border line between each alarm or timer that appears in the card. Currently it shows like this:

image

Sorry if there is a simpler way of achieving this, but I thought having the additional class/es might help others with styling too?