Javascript Custom Timer Card not Updating UI

Hello,
I’m trying to code a custom card that can render multiple lines of grouped together entitites. Each row should hold a timer, switch and time-input entity.

What I got so far is the following:

import {
  LitElement,
  html,
  css
} from "https://unpkg.com/[email protected]/lit-element.js?module";


class CustomButtonCountdownCard extends LitElement {
    static get properties() {
        return {
            _hass: Object,
            config: Object,
            stateObj: Object,
            _timeRemaining: {state: true},
            _interval: {state: true}
        };
    }
      
    constructor(){
        super();
        this._timeRemaining={};
        this._interval={};
    }
  
  
  
    set hass(hass) {
        this._hass = hass;

        if (hass && this.config) {
            
            this.entityIds=[];
            this.timerStates=[];
            this.rows = this.config.rows.map((singleRowConfig)=>{
                //map each single row
                let retval = JSON.parse(JSON.stringify(singleRowConfig));
                let k;
                for (k of Object.keys(singleRowConfig)) {
                    let key = k;
                    let object = singleRowConfig[k];
                    retval[key].stateObj = object.entity ? hass.states[retval[key].entity] : this.stateObj;
                    this.entityIds.push(object.entity);
                    if(object.entity.startsWith("timer.")){
                        this.timerStates.push(retval[key]);
                    }
                }
                return retval;
            });
        }
    }
    
    get hass(){
        return this._hass;
    }

  setConfig(config) {
    if (!config.rows) throw new Error("You need to define rows");
    if (!config.rows[0]["switch-entity"]) throw new Error("You need to define switch-entity");
    if (!config.rows[0]["timer-entity"]) throw new Error("You need to define timer-entity");
    if (!config.rows[0]["timer-target-entity"]) throw new Error("You need to define timer-target-entity");
    if (!config.rows[0]["automation-entity"]) throw new Error("You need to define automation-entity");
    this.config = config;
  }
  
  static get styles() {
        return css`
      .myHost {
        display: flex;
        align-items: center;
        flex-direction: row;
      }
      .info {
        margin-left: 16px;
        margin-right: 8px;
        flex: 1 1 30%;
      }
      .info,
      .info > * {
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      .flex ::slotted(*) {
        margin-left: 8px;
        min-width: 0;
      }
      .flex ::slotted([slot="secondary"]) {
        margin-left: 0;
      }
      .secondary,
      ha-relative-time {
        color: var(--secondary-text-color);
      }
      state-badge {
        flex: 0 0 40px;
      }
      .myHost([rtl]) .flex {
        margin-left: 0;
        margin-right: 16px;
      }
      .myHost([rtl]) .flex ::slotted(*) {
        margin-left: 0;
        margin-right: 8px;
      }
      .pointer {
        cursor: pointer;
      }
      .state {
        text-align: right;
      }
      .state.rtl {
        text-align: left;
      }
      .value {
        direction: ltr;
      }`;
    }
    
    
    getEntityIds(){
        return this.entityIds;
    }
    
    hasConfigOrEntitiesChanged(node, changedProps){
        if (changedProps.has('config')) {
            return true;
        }
        const oldHass = changedProps.get('_hass');
        if (oldHass) {
            return node.entityIds.some((entity) => oldHass.states[entity] !== node._hass.states[entity]);
        }
        return false;
    }
    
    shouldUpdate(changedProps) {
        const updt = this.hasConfigOrEntitiesChanged(this, changedProps);
        //console.log("updt: ", updt);
        return updt;
    }
        
    hasUpdated(changedProps){
        //console.log("has updated");
        if (!this.config || !changedProps.has("_hass")) {
          return;
        } 
        
        const oldHass = changedProps.get("_hass");
        this.timerStates.forEach((timerState)=>{
            const stateObj_fn = this._hass.states[timerState.entity];
            const oldStateObj = oldHass
                  ? oldHass.states[timerState.entity]
                  : undefined;
            if (oldStateObj !== stateObj_fn) {
              this._startInterval(timerState.entity, stateObj_fn);
            } else if (!stateObj_fn) {
              this._clearInterval(timerState.entity);
            }
        });
    }

    updated(changedProps) {
      super.updated(changedProps);
      this.hasUpdated(changedProps);
    }

    _calculateRemaining(entityId, stateObj_fn) {
        console.log("_calculateRemaining", stateObj_fn);
        if(!stateObj_fn){
            this._timeRemaining[entityId] = 0;
            return;
        }
        if(!stateObj_fn.attributes.remaining){
                this._timeRemaining[entityId] = 0;
                return;
        }
        let a = stateObj_fn.attributes.remaining.split(':'); // split it at the colons
        // minutes are worth 60 seconds. Hours are worth 60 minutes.
        let seconds = (+a[0]) * 60 * 60 + (+a[1]) * 60 + (+a[2]); 
        
        this._timeRemaining[entityId] = seconds;
        console.log("_timeRemaining", this._timeRemaining[entityId]);
      }

    _clearInterval(entityId) {
        console.log("_clearInterval");
        if (this._interval[entityId]) {
          window.clearInterval(this._interval[entityId]);
          this._interval[entityId] = undefined;
        }
    }
      
    _startInterval(entityId, stateObj_fn) {
        this._clearInterval(entityId);
        this._calculateRemaining(entityId, stateObj_fn);
        
        if (stateObj_fn.state === "active") {
            console.log("setting interval");
              this._interval[entityId] = window.setInterval(
                () => {
                    console.log("st: ", stateObj_fn);
                    //const stat = 
                    console.log(this.timerStates);
                    this._calculateRemaining(entityId, stateObj_fn);
					//stateObj_fn always the same leeds to not updating ui
                },
            1000
            );
        }
    }

  getCardSize() {
    return this.config.entities.length + 1;
  }


    render() {
        if (!this._hass || !this.config) return html`error`;
        
        return html `
            <ha-card>
                <div class="card-content">
                    <div class="myHost">
                        <state-badge .stateObj=${this.rows[0]["switch-entity"].stateObj}></state-badge>
                        <div class="info  pointer text-content " title="Schalter TEST">
                            Schalter
                        </div>
        
                        <div .hass="${this._hass}">${this.rows[0]["timer-entity"].stateObj.attributes.remaining}</div>
                        
                        <ha-entity-toggle
                                .hass="${this._hass}"
                                .stateObj="${this.rows[0]["switch-entity"].stateObj}"
                                >
                        </ha-entity-toggle>
                        
                        
                        <ha-time-input
                                .hass="${this._hass}"
                                .stateObj="${this.rows[0]["timer-target-entity"].stateObj}">
                        
                        </ha-time-input>
                        
                        
                    </div>
                </div>
            </ha-card>`;
    }
}
customElements.define('custom-button-countdown-card', CustomButtonCountdownCard);

One cann configure that with following example:

type: custom:custom-button-countdown-card
rows:
  - switch-entity:
      entity: input_boolean.schalter_test
      name: guenther
    timer-entity:
      entity: timer.test
    timer-target-entity:
      entity: input_datetime.test_time
    automation-entity:
      entity: automation.test_automatisierung_zeit

The switch works so far but the remaining time of the timer does not update while ticking. Only status change of the timer updates the remaining time in the UI.

Can anyone help with this?

I’m Using Javascript and included the card.js file as an Resource in the configuration.
Remind that this is my first time coding javascript in Homeassistant (not coding in general).
Its a first prototype the should start an editable timer on switch toggle and show the time till the switch will be automatically toggled back. All the Logic should be done with automations I guess.

regards, appreciate any help.

Manuel

1 Like