I must have missed some step along the way or maybe I misunderstood … Any help would be appreciated…
I have the alexa media player installed and can see all the _next_timer entities for my alexa devices.
I use studio code server to handle files such as config.
I created config\www\card-alexa-timers.js and it seems OK. I then added to my resources page as /local/card-alexa-timers.js and have restarted HA but when I try to use the integration I get No card type found or No card type configured.
I don’t see any errors in the logs
Is there a way to check what the issue is? Can I confirm the js loaded ok? (my skill is not in js)
Hey There! First - thanks for this card. It’s exactly what I was looking for.
One thing I’m struggling to figure out is how to increase the font size of the timers themselves for a tablet display. I was able to do this with the card-mod, but the text pulses (starts small and then increases to the font size I’ve set below). Is there a better way to do this? Thanks!
I vaguely remember having a similar problem, but it’s been a while. This is what I’m using, inside my theme’s .yaml code, to set it to 60px for my wall display to be easy to read. Not sure if the transition is what’s making it not resize. I can’t recall.
I’m now trying to figure out how to create 3 custom sensors for up to 3 active timers. I need them as sensors because the OpenHASP display project only takes sensors as parameters for the display.
Here’s sort of a mockup of where I’m going to put the timer text:
It can be on any page, but it needs to get its data from an existing sensor
Perhaps I could create 3 pairs of text “helpers” and then populate them in this part of your code?
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);
table += "<tr>";
table += "<td>" + name + "</td>";
table += "<td>" + timeLeft + "</td>";
table += "</tr>";
//something like this (Im NOT a js coder, but this is basically what I want to do, I just dont know how):
sensor.alexa_timer_(i)_name == name
sensor.alexa_timer_(i)_timeleft == timeLeft
}
@jazzmonger I code a lot in JavaScript, but this is my first Home Assistant module using JavaScript. That part of the code is looping through the collected alarms, so that would be the right place to put code that updates sensors. My code uses sensors, but only to read from them (it’s based on Custom Cards at Custom Cards | Home Assistant Developer Docs).
Time for wild guesses!!
It looks like theoretically you should be able to set the sensor’s values, but it’s probably going to be something more like this (PLEASE NOTE I HAVE NOT TESTED THIS):
There’s probably a more elegant way of doing this, but my brain is a bit tired. Also in order for this to work, you need to create your own sensors in the configuration.yaml file (or an included file if you’re like me), which would maybe look something like this (AGAIN, NOT TESTED):
I’d suggest asking in a separate post, maybe in the Development forum? It feels to me like there’s a better way of doing this, and what I posted above is just a wild guess at how I would do this. It feels like there’s probably a way of giving custom sensors properties, so maybe you could have a sensor.alexa_timer_0 sensor with a “name” and “timeleft” property. But I don’t know the right syntax for any of this. I also don’t know if the sensor you create needs to have a default value of some kind, or if “template” is the right platform (I’m kind of flying by the seat of my pants in Home Assistant). I think what you’re trying to do should be straightforward, but I don’t know the right magic words to make it work. Good luck!
Awesome. Interesting ideas. I’ll try them tomorrow when I’m not on an iPad!
I agree, I will also create a new post. This is complicated stuff. I’m honestly not sure why the folks writing the Alexa integration didn’t expose the timers. It’s such a basic feature that is used by everyone, and countless times a day.
@jazzmonger The more I look into this, the more I think that you might not be able to change the sensors with a module. Home Assistant is updating the hass object in the module, and I don’t think it’s ever affecting anything external to the card. Basically, you can only read, not write from there.
However, I think maybe what you want can be done with the yaml if you only have one Alexa device.
A kind of pseudocode for this would look like this:
- platform: template
sensors:
alexa_timer_0_name:
unique_id: sensor.alexa_timer_0_name
value_template: {get the name of the 0th timer on the device}
- platform: template
sensors:
alexa_timer_0_timeleft:
unique_id: sensor.alexa_timer_0_timeleft
value_template: {get and format the time left of the 0th timer on the device}
Otherwise you may need to write an integration, and think those are done in Python, which I am only passingly familiar with. Sorry, this seems to be out of my depth.
@Kethlak Hi Cassandra
This has ben a really useful bit of script so many thanks for sharing it.
I recently installed Home Assistant 2023.4.1 and when I checked the log I see a lot of errors related to the script … I don’t recall seeing these before but maybe I was not looking … I don’t always check the log. it looks like an initiation issue as once everything is online they shop … I am guessing the timer returning unavailable until that point.
http://:8123/local/card-alexa-timers.js:38:58 Uncaught TypeError: Cannot read properties of undefined (reading ‘attributes’)
17:43:10 – (ERROR) components/system_log/init.py - message first occurred at 17:40:01 and shows up 374 times
Is anyone else seeing this or is it just me? I am happy to ignore them but would ideally like to see if I can avoid them … is there a change that could be made to either wait or check for unavailable or delay start up… it may just be a timing issue and some process order or speed of start up has changed. Many thanks
@snigehere Yeah, that does look new. I hadn’t upgraded yet, so I checked before and they weren’t there.
I should be able to take a look at it in the next few days.
@snigehere Okay, I fixed it. For some reason, something is getting passed as an entity that it’s not expecting. Here’s my current code.
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%'>";
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);
table += "<tr>";
table += "<td>" + name + "</td>";
table += "<td>" + timeLeft + "</td>";
table += "</tr>";
}
table += "</table>";
this.content.innerHTML = 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;
}
customElements.define('card-alexa-timers', CardAlexaTimers);
Basically, I added an if statement right inside the for loop, wrapping lines 39-48. If it gets an undefined entity, it doesn’t try to get its properties. It still seems to be working for me. I’m not sure what entity it’s getting that is undefined or why, and I’m too lazy to figure it out. It’s probably not my code anyway, seeing as this is a new issue.
@snigehere I forgot to add (in case you’re not aware) that HA likes to cache the resources, so if you make a change in the code, you’ll probably need to change the resource reference at Settings > Dashboards > click the 3 dots in the upper right hand corner of the page and select resources. I mentioned a while ago in this thread that you can just change the name to [whatever your filename was].js?v=[a number] so for example, mine is /local/card-alexa-timers.js?v=101 since I’m on version 101 of this thing. Then you’ll need to clear the cache of any browser you’re using to view dashboards, or it will keep trying to retrieve the old file. Like I have a tablet hanging on my wall to view the timers and such, and I needed to clear the cache of it (I use Fully Kiosk, and it worked to just close the program and open it again) in order for it to stop throwing errors in the log.
Nice card! Anyway to support alarms too? I tried adding a _next_alarm sensor and your card did not work for me. I wrote some markdown a couple years back that supports both timers and alarms, but your formatting is way nicer. I do not understand how to convert it to a lovelace card: https://community.home-assistant.io/t/alexa-timer-card/258234/28?u=jaaem ,
@Kethlak Thanks so much for sharing this code - I came across this thread when searching to find out if something like this was possible, and set it all up from my phone - it worked as a new card on my test dashboard straight away Now I just have to find somewhere suitable to display it!
@jaaem Yeah, it looks like alarms are a different data type. However, I wrote this code which you could include as a separate resource, and a separate custom card, that will pull all alarms from all devices.
class CardAlexaAlarms 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(Date.parse(alarm.date_time) >= Date.now()) {
this.alarms.push(new AlexaAlarm(alarm.alarmLabel, alarm.date_time));
}
}
}
}
}
this.alarms.sort((a,b) => a.alarmTime - b.alarmTime);
if (this.alarms.length === 0) {
this.innerHTML = '';
this.content = null;
}
if(this.alarms.length > 0) {
this.innerHTML = `
<ha-card class="alexa-alarms">
<div class="card-content"></div>
</ha-card>
`;
this.content = this.querySelector('div');
let table = "<table border='0' width='100%'>";
for(let i = 0; i < this.alarms.length; i++) {
let name = this.alarms[i].name;
if(name === null) {
name = getNameFromDuration(this.alarms[i].originalDurationInMillis);
}
let timeLeft = "";
if(this.alarms[i].days > 0) {
timeLeft = this.alarms[i].days + "days, ";
}
timeLeft += this.alarms[i].hours + ":" + addLeadingZero(this.alarms[i].minutes) + ":" + addLeadingZero(this.alarms[i].seconds);
table += "<tr>";
table += "<td>" + name + "</td>";
table += "<td>" + timeLeft + "</td>";
table += "</tr>";
}
table += "</table>";
this.content.innerHTML = table;
}
}
}
class AlexaAlarm {
constructor(label, date_time) {
this.name = label;
if(this.name == null) {
this.name = getNameFromTime(date_time);
}
this.alarmTime = date_time;
this.remainingTime = Date.parse(date_time) - Date.now();
this.seconds = Math.floor(this.remainingTime / 1000);
this.minutes = Math.floor(this.seconds / 60);
this.hours = Math.floor(this.minutes / 60);
this.days = Math.floor(this.hours / 24);
this.seconds = this.seconds % 60;
this.minutes = this.minutes % 60;
this.hours = this.hours % 24;
}
}
function addLeadingZero(num) {
if(num < 10) {
return "0" + num;
}
else {
return num.toString();
}
}
function getNameFromTime(alarmTime) {
let alarmDateTime = new Date(alarmTime);
let today = new Date(Date.now());
let isAlarmToday = false;
if(alarmDateTime.getFullYear() == today.getFullYear() && alarmDateTime.getDate() == today.getDate() && alarmDateTime.getMonth() == today.getMonth()) {
isAlarmToday = true;
}
let name = "";
if(isAlarmToday) {
name = "Today at ";
}
else {
name = getMonthNameFromNumber(alarmDateTime.getMonth()) + " " + alarmDateTime.getDate() + " at ";
}
name += addLeadingZero(alarmDateTime.getHours()) + ":" + addLeadingZero(alarmDateTime.getMinutes());
return name;
}
function getMonthNameFromNumber(monthNum) {
switch(monthNum) {
case 0:
return "January";
case 1:
return "February";
case 2:
return "March";
case 3:
return "April";
case 4:
return "May";
case 5:
return "June";
case 6:
return "July";
case 7:
return "August";
case 8:
return "September";
case 9:
return "October";
case 10:
return "November";
case 11:
return "December";
default:
return null;
}
}
customElements.define('card-alexa-alarms', CardAlexaAlarms);
And then in your dashboard yaml, you’ll write something like
If you’ve gotten the timers working, you know how to get this one working as well.
It could probably be rewritten into a single module, but eh. If you wanted to do that, you’d need to have two types of entities (which is perfectly doable, by the way) and treat them differently.
We don’t tend to use alarms as much, since we have an echo clock and the alarm noise is really quiet (I know it’s probably changeable, just too lazy to change it, but not too lazy to code for 3 hours for something I’m not planning on using) and we’ve just found that timers work better for us.