Lovelace: Munich public transport departure card

Playing around with the awesome new Lovelace UI, I created a custom card to show live departure times for public transport in Munich, using the MVG Live sensor. (To make this possible I also had to modify the sensor and will submit the modifications as a PR soon.)

Example result:

You can find the JS in this GIST (but it won’t work until the MVG sensor is updated…).

9 Likes

Amazing looking component!

You could release a custom component of that patch until it’s out. Also some css variables would be nice if you want to allow customisation of the card in the future and to match theme of user

Also could create dom in setConfig and use set (hass) only for updates if state is different.

This card works very well, but I found a little bug. When the MVG Live sensor does not return any connection, there is a lovelace exception thrown in my UI:

https://.../local/mvg-card.js:108:46 Uncaught TypeError: state.attributes.departures is not iterable

2 Likes

Does this card work out of the box now? Where do I install the JS file from github?

I haven’t used it for a while myself, would be grateful if you could test it.

With the JS file it works as usual: put it into your <config>/www and add it to the Lovelace config under resources.

Hi could you help me out?
I put your js in config/www/custom_lovelace/mvg_card.js
Added it as a resource in Lovelace Settings as /local/mvg_card.js (and also /local/custom_lovelace/mvg_card.js because i was not sure)
And tried to add it to Lovelace with - type: 'custom:mvg-card'
But i only get:

You’re missing a space between - and t

The problem was either the double entry of the mvg-card.js in the resources or the missing entity.

With this it worked:

type: 'custom:mvg-card'
entity: sensor.LOCATION_NAME

No - needed at all in the card editor.

1 Like

Hi guys,

can you tell me if the mvg-card.js still works and maybe where I have to put it and which config I have to change to use this card ?

Br

Hey there!
Joined HomeAssistant Fam 2 days ago and was really interested to use this, but didn’t get it to work. I’ve tried a lot and in the end, it worked for me.

Idk how and why, I’m just grateful it works.

Notes:

  • In your lovelace dashboard, put the card into a conditional card, like this:
type: conditional
conditions:
  - entity: sensor.YOUR_DEPARTURE
    state_not: '-'
card:
  type: custom:new-mvg-card
  entity: sensor.YOUR_DEPARTURE

Because if the next departure is more than 2h away, the sensor’s state goes to ‘-’, And the card wouldn’t display anything

  • The sensor’s direction attribute doesn’t seem to be supported by mvg anymore, at least for mine it always retrieved an empty field for each departure. If you’re not getting any data, delete that condition out of your config file

And this is the code. Call the js file new-mvg-card.js and it should work without problems (hopefully)

class NewMvgCard extends HTMLElement {
  // Whenever the state changes, a new `hass` object is set. Use this to
  // update your content.
  set hass(hass) {
    
    
    const entityId = this.config.entity;
    const state = hass.states[entityId];
    const stateStr = state ? state.state : 'unavailable';
    const name = state.attributes['friendly_name'];
    
    if (!this.content) {
      const card = document.createElement('ha-card');
      card.header = name;
      this.content = document.createElement('div');
      const style = document.createElement('style');
      style.textContent = `
        table {
          width: 100%;
          padding: 6px 14px;
        }
        td {
          padding: 3px 0px;
        }
        td.shrink {
          white-space:nowrap;
        }
        td.expand {
          width: 99%
        }
        span.line {
          font-weight: bold;
          font-size:0.9em;
          padding:3px 8px 2px 8px;
          color: #fff;
          background-color: #888;
          margin-right:0.7em;
        }
        span.S-Bahn {
          border-radius:999px;
        }
        span.U-Bahn {
        }
        span.Tram {
          background-color: #ee1c25;
        }
        span.Bus {
          background-color: #005f5f;
        }
        span.U1 {
          background-color: #3c7233;
        }
        span.U2 {
          background-color: #c4022e;
        }
        span.U3 {
          background-color: #ed6720;
        }
        span.U4 {
          background-color: #00ab85;
        }
        span.U5 {
          background-color: #be7b00;
        }
        span.U6 {
          background-color: #0065af;
        }
        span.U7 {
          background-color: #c4022e;
        }
        span.U8 {
          background-color: #ed6720;
        }
        span.S1 {
          background-color: #16c0e9;
        }
        span.S2 {
          background-color: #71bf44;
        }
        span.S3 {
          background-color: #7b107d;
        }
        span.S4 {
          background-color: #ee1c25;
        }
        span.S5 {
          background-color: #ffcc00;
          color: black;
        }
        span.S6 {
          background-color: #008a51;
        }
        span.S7 {
          background-color: #963833;
        }
        span.S8 {
          background-color: black;
          color: #ffcb06;
        }
        span.S20 {
          background-color: #f05a73;
        }
        `
      card.appendChild(style);
      card.appendChild(this.content);
      this.appendChild(card);
    }

    var tablehtml = `
    <table>
    `
    if(state != '-'){
        for (const attributes of state.attributes['departures']) {
          const icon = attributes['icon']
          const destination = attributes['destination']
          const linename = attributes['linename']
          const product = attributes['product']
          const time = attributes['time']
    
          const iconel = document.createElement('ha-icon');
          iconel.setAttribute('icon', icon);
          const iconHTML = iconel.outerHTML;
    
          tablehtml += `
              <tr>
                <td class="shrink" style="text-align:center;"><span class="line ${product} ${linename}">${linename}</span></td>
                <td class="expand">${destination}</td>
                <td class="shrink" style="text-align:right;">${time}</td>
              </tr>
          `;
        }
    }
    tablehtml += `
    </table>
    `

    this.content.innerHTML = tablehtml
  }

  // The user supplied configuration. Throw an exception and Lovelace will
  // render an error card.
  setConfig(config) {
    if (!config.entity) {
      throw new Error('You need to define an entity');
    }
    this.config = config;
  }

  // The height of your card. Home Assistant uses this to automatically
  // distribute all cards over the available columns.
  getCardSize() {
    return 1;
  }
}

customElements.define('new-mvg-card', NewMvgCard);

// Configure the preview in the Lovelace card picker
window.customCards = window.customCards || [];
window.customCards.push({
  type: 'new-mvg-card',
  name: 'MVG Card',
  preview: false,
  description: 'This card displays departures for a sensor created with mvglive',
});

Hi manuel19,

can your share your yaml code from card etc. ?

br

After adding, you should be able to add the card with the graphical interface. From there you only have to add your entity name. In total it should look like this:

type: custom:new-mvg-card
entity: sensor.abfahrten_siemenswerke

(Or whatever your sensor is named)

Is it possible, and if so how, that it also shows me delays?

Furthermore, I would like to have a text with minutes / min after the time display.

Is mvglive broken for anyone else? Stopped showing anything not long ago.

yep, it stopped working with the last change of the timetable.
I’m currently testing the HAFAS integration https://github.com/akloeckner/hacs-hafas. It works for the S Bahn, I’ve not tried any other transportations, and gives the next three connections plus delay of the first one.

Hello everyone,

Since I recently moved to Munich, I started looking for an MVG integration. There are currently several integrations with different versions circulating through the forum:

The official MVGLIVE integration

Fixed MVGLIVE integration

Version by MrGauz

I also found this Lovelace card:

In short, I have now spent the last few hours building a new version from the two custom cards. It works with the current version of danielpotthast.

// MVG Timetable Card

// Inspirations:
//  - https://gist.github.com/thomasloven/1de8c62d691e754f95b023105fe4b74b
//  - https://github.com/MrGauz/home-assistant-munich-transport/blob/main/www/munich-transport-timetable-card.js
//  - https://community.home-assistant.io/t/lovelace-munich-public-transport-departure-card/59622/11

class MVGTimetableCard extends HTMLElement {
    constructor() {
        super();
        this.attachShadow({
            mode: 'open'
        });
    }

    /* This is called every time sensor is updated */
    set hass(hass) {

        const config = this._config;
        const maxEntries = config.max_entries || 10;
        const showStopName = config.show_stop_name === false ? false : true;
        const entityIds = config.entity ? [config.entity] : config.entities || [];
        const destinationMaxLen = config.direction_name_max_length || 40;

        let content = "";

        for (const entityId of entityIds) {
            const entity = hass.states[entityId];
            if (!entity) {
                throw new Error("Entity State Unavailable");
            }

            if (showStopName) {
                content += `<div class="stop">${entity.attributes.friendly_name}</div>`;
            }

/*
            if (!entity.attributes.departures || !entity.attributes.departures.slice(0, maxEntries).length) {
                content += `Keine Abfahrten in den nächsten 10 Minuten :/`;
            }
*/

if (!entity.attributes.departures || entity.attributes.departures.length < 1) {
    content += `Keine Abfahrten in den nächsten 10(?) Minuten :/`;
} 
else {

            const timetable = entity.attributes.departures.slice(0, maxEntries).map((departure) =>
                `<div class="departure">
                    <div class="line">
                        <span class="line-icon ${departure.type} ${departure.line}">${departure.line}</span>
                    </div>
                    <div class="direction">${(departure.destination.length > destinationMaxLen)
                        ? departure.destination.slice(0, destinationMaxLen - 3) + '...'
                        : departure.destination}</div>
                    <div class="time">${departure.time_in_mins} min</div>
                </div>`
            );

            content += `<div class="departures` + (showStopName ? ' padding' : '') + `">` + timetable.join("\n") + `</div>`;
            
        }

        }

        this.shadowRoot.getElementById('card-content').innerHTML = content;
    }

    /* This is called only when config is updated */
    setConfig(config) {
        this._config = config;
        
        if(!config.entity && !config.entities) {
            // If no entity was specified, this will display a red error card with the message below
            throw new Error('You need to define at least one entity');
        }

        const root = this.shadowRoot;
        if (root.lastChild) root.removeChild(root.lastChild);

        const card = document.createElement('ha-card');
        const content = document.createElement('div');
        const style = document.createElement('style');

        style.textContent = `
            div.stop {
                opacity: 0.6;
                width: 100%;
                text-align: left;
                padding: 5px 0px;
            }      
            div.departure {
                padding: 3px 0px;
                display: flex;
                flex-direction: row;
                flex-wrap: nowrap;
                align-items: flex-start;
                gap: 20px;
            }     
            div.padding {
                padding-left: 20px
            }
            div.line {
                padding: 2px; 
            }
            span.line-icon {
                padding: 3px 8px;
                font-weight: bold;
                font-size: 0.9em
                color: #FFF;
                margin-right:0.7em;
            }
            div.direction {
                align-self: center;
                flex-grow: 1;
            }
            div.time {
                align-self: flex-start;
            }
            span.S-Bahn {
                border-radius:999px;
            }
            span.U-Bahn {
            }
            span.Tram {
                background-color: #ee1c25;
            }
            span.Bus {
                background-color: #005f5f;
            }
            span.U1 {
                background-color: #3c7233;
            }
            span.U2 {
                background-color: #c4022e;
            }
            span.U3 {
                background-color: #ed6720;
            }
            span.U4 {
                background-color: #00ab85;
            }
            span.U5 {
                background-color: #be7b00;
            }
            span.U6 {
                background-color: #0065af;
            }
            span.U7 {
                background-color: #c4022e;
            }
            span.U8 {
                background-color: #ed6720;
            }
            span.S1 {
                background-color: #16c0e9;
            }
            span.S2 {
                background-color: #71bf44;
            }
            span.S3 {
                background-color: #7b107d;
            }
            span.S4 {
                background-color: #ee1c25;
            }
            span.S5 {
                background-color: #ffcc00;
                color: black;
            }
            span.S6 {
                background-color: #008a51;
            }
            span.S7 {
                background-color: #963833;
            }
            span.S8 {
                background-color: black;
                color: #ffcb06;
            }
            span.S20 {
                background-color: #f05a73;
            }
        `;

        content.className = "card-content";
        content.id = "card-content";
        card.header = config.title;
        card.appendChild(content);
        card.appendChild(style);

        root.appendChild(card);
    }

    // The height of the card.
    getCardSize() {
        return 5;
    }
}

customElements.define('MVG-Timetable-Card', MVGTimetableCard);

  // Configure the preview in the Lovelace card picker
  window.customCards = window.customCards || [];
  window.customCards.push({
    type: 'MVG-Timetable-Card',
    name: 'MVG Timteable Card',
    preview: false,
    description: 'This card displays departures for a sensor created with mvglive',
  });

Lovelace-Code:

  - type: custom:mvg-timetable-card
    title: MVG Live
    show_stop_name: true
    entities:
      - sensor.rosenheimer_platz

Result:
image

Unfortunately, there are virtually no detailed tutorials for creating custom cards. There are still a few issues in mine (e.g. working preview via setConfig()). I would be very happy if someone here could find good documentation or tutorials for this. The whole topic is only touched on very superficially on the developer pages.

Well, it works well at the moment. Maybe there is someone else here who can improve it a bit :wink:

Have fun with it and best regards from Munich!

1 Like

Dear blackmesa,

could you share your current configuration ?

Br
boxi