🏠 HaCasa - A new modern dashboard

If it were possible to install it from HACS, I would have already installed it. Nice and sophisticated, great job. I will pay attention to the channel and if possible I will be one of the first to install it.

2 Likes

Download the Source code (zip) from the latest release
maybe it would of been better to post this thread when everything was ready to go !

1 Like

thanks for this!! some great ideas in it that i can use to change my own button cards… the way you have set things up is very very nice

1 Like

What I’m missing is an entity-card and foremost a template-card. I have so many things, that I need to show in some way, a template-card is nice in such cases!

But overall it’s a good feedback, I think! :slight_smile:

Anyway to install this via HACs?

The answer is given above: no

My bad! I fix this as soon as possible!

That is a great idea, i will work on that.

1 Like

This will be my prio one to find out how :sweat_smile:

1 Like

Looks crazy good dude!

When i add this to my configuration.yaml I am getting an error. See below

lovelace:
  mode: "storage"
  resources:
    - url: "/hacsfiles/button-card/button-card.js"
      type: "module"
    - url: "/hacsfiles/my-cards/my-cards.js"
      type: "module"
    - url: "/hacsfiles/kiosk-mode/kiosk-mode.js"
      type: module
    - url: "https://fonts.googleapis.com/css2?family=Montserrat:wght@100;200;300;400;500;600;700;800;900"
      type: css
  dashboards:
    HaCasa:
      mode: "yaml"
      title: HaCasa
      icon: mdi:script
      show_in_sidebar: true
      filename: "dashboard/HaCasa/main.yaml"
The system cannot restart because the configuration is not valid: Invalid config for 'lovelace' at configuration.yaml, line 20: Url path needs to contain a hyphen (-) for dictionary value 'lovelace->dashboards', got {'HaCasa': {'mode': 'yaml', 'title': 'HaCasa', 'icon': 'mdi:script', 'show_in_sidebar': True, 'filename': 'dashboard/HaCasa/main.yaml'}}

That’s an error in the documentation, you’re totally right.

The name for a dashboard needs to contain a hyphen, so in this case, you could take something like “Ha-Casa”. Or hacasa-test. Whatever you like, but with a hyphen.

I’d use something like this:

lovelace:
[...]
  dashboards:
    hacasa-test:
[...]

EDIT:
See here for the documentation:


@damiantje99t
I’m not familiar with how you setup your Github repo, so I choose to not submit a PR to change that line. :slight_smile: But you should do it, as it hinders people to install it… I haven’t noticed, but I didn’t use the original name for my test dashboard… :slight_smile:

I like the overall look of the theme… Nice and classy, although I think there is room for improvements.
The first one is definatly getting the possibility to install this using HACS.
And the second is getting rid of the dependencies of the other modules.
The use of templates is great, but raises the bar of a simple theme installation.

Hi, nice theme template :ok_hand: I saw on the repo that you have an issue to apply to the template progress bar for media players. I have this universal template that I use for my other custom button cards. I tried to insert it into your template and it seems to work including real time update.

  progress_bar:
    styles:
      custom_fields:
        progress:
          - background-color: 'rgba(0,0,0,0.1)'
          - position: absolute
          - top: unset
          - bottom: 0px
          - height: 0.5rem
          - width: 100%

        bar:
          - background-color: '#00acc1'
          - position: absolute
          - bottom: 0px
          - left: 0%
          - top: unset
          - height: 0.5rem
          - z-index: 1
          - transition: 1s ease-out
    custom_fields:
      bar: >
        [[[
          if (entity.attributes.media_position !== undefined) {
          setTimeout(() => {
            let elt = this.shadowRoot,
                card = elt.getElementById('card'),
                container = elt.getElementById('container'),
                bar = elt.getElementById('bar');
            if (elt && card && container && bar) {
                card.insertBefore(bar, container);
                  function update() {
                    let mediaPositionUpdatedAt = entity.attributes.media_position_updated_at,
                        mediaPosition = entity.attributes.media_position,
                        mediaDuration = entity.attributes.media_duration,
                        mediaContentType = entity.attributes.media_content_type;
                    let percentage = entity.state === 'paused'
                      ? (mediaPosition / mediaDuration * 100)
                      : entity.state === 'playing'
                        ? (((Date.now() / 1000) - (new Date(mediaPositionUpdatedAt).getTime() / 1000) + mediaPosition) / mediaDuration * 100)
                        : 0;
                    bar.style.width = percentage.toFixed(1) + '%';
                    requestAnimationFrame(update);
                  }
                  requestAnimationFrame(update);
            }
          }, 0);
          return ' ';}
        ]]]
      progress: >
        [[[
          if (entity.attributes.media_position !== undefined) {
          return ' ';}
        ]]]

example of use

- type: custom:button-card
  template:
    - custom_card_mediaplayer_music
    - progress_bar
  entity: media_player.spotify

progressbar

2 Likes

How does the navigation-bar works. Do you have code for that?

Awesome work! As MiniHass is somewhat dormant for personal development, it’ll be fun to follow this little project too :slight_smile:

Nice work! Going to have a play… :slight_smile:

This in combination with Bubble Card could be very polished indeed!

Hello, I am trying to implement your progress bar and it is working perfectly but I would like to show the remaining time as text as it decreases in addition to the bar. Do you know any way to do it? The value is somewhere in the function but I don’t know how to get it out of it. Thanks in advance.

EDIT: I did it myself, I’m not a programmer but it works

type: custom:button-card
entity: media_player.homepod_del_salon
show_icon: false
show_name: false
custom_fields:
  total: |
    [[[
        if (entity.attributes.media_duration !== undefined){
              var total = entity.attributes.media_duration; 
              return new Date(total * 1000).toISOString().substring(14, 19);
        }
    ]]]
  restante: |
    [[[
      if (entity.attributes.media_position !== undefined) {
      return ' ';}
    ]]]
  bar: |
    [[[
      if (entity.attributes.media_position !== undefined) {
      setTimeout(() => {
        let elt = this.shadowRoot,
            card = elt.getElementById('card'),
            container = elt.getElementById('container'),
            bar = elt.getElementById('bar');
        if (elt && card && container && bar) {
            card.insertBefore(bar, container);
              function update() {
                let mediaPositionUpdatedAt = entity.attributes.media_position_updated_at,
                    mediaPosition = entity.attributes.media_position,
                    mediaDuration = entity.attributes.media_duration,
                    mediaContentType = entity.attributes.media_content_type;
                let percentage = entity.state === 'paused'
                  ? (mediaPosition / mediaDuration * 100)
                  : entity.state === 'playing'
                    ? (((Date.now() / 1000) - (new Date(mediaPositionUpdatedAt).getTime() / 1000) + mediaPosition) / mediaDuration * 100)
                    : 0;
                let restante = entity.state === 'paused'
                  ? (mediaDuration - mediaPosition)
                  : entity.state === 'playing'
                    ? (mediaDuration - ((Date.now() / 1000) - (new Date(mediaPositionUpdatedAt).getTime() / 1000) + mediaPosition) )
                    : 0;
                bar.style.width = percentage.toFixed(1) + '%';
                elt.getElementById("restante").innerHTML = new Date(restante.toFixed(0) * 1000).toISOString().substring(14, 19);
                requestAnimationFrame(update);
              }
              requestAnimationFrame(update);
        }
      }, 0);
      return ' ';}
    ]]]
  progress: |
    [[[
      if (entity.attributes.media_position !== undefined) {
      return ' ';}
    ]]]
styles:
  grid:
    - grid-template-areas: '"total bar restante"'
    - grid-template-rows: min-content
    - grid-template-columns: min-content 1fr min-content
  custom_fields:
    progress:
      - background-color: rgba(0,0,0,0.1)
      - position: absolute
      - top: unset
      - bottom: 0px
      - height: 0.5rem
      - width: 100%
    bar:
      - background-color: '#00acc1'
      - position: absolute
      - bottom: 0px
      - left: 0%
      - top: unset
      - height: 0.5rem
      - z-index: 1
      - transition: 1s ease-out

Small update: I’m pretty overwhelmed about all the positive reactions and ideas, I’m so happy people like it.

Last week was pretty busy so I’m going to note everything and get to work next week!

1 Like

this custom field should update according to the playing state, but there is some delay, probably because of the button card property how often it updates entities.

  progress_bar:
    styles:
      custom_fields:
        progress:
          - background-color: 'rgba(0,0,0,0.1)'
          - position: absolute
          - top: unset
          - bottom: 0px
          - height: 0.5rem
          - width: 100%
        bar:
          - background-color: '#00acc1'
          - position: absolute
          - bottom: 0px
          - left: 0%
          - top: unset
          - height: 0.5rem
          - z-index: 1
          - transition: 1s ease-out
        time:
          - position: absolute
          - bottom: 10px
          - right: 0px
          - color: var(--primary-text-color)
          - font-size: 0.7rem
          - padding: 0px 5px
          - z-index: 1
    custom_fields:
      time: >
        [[[
          if (entity.attributes.media_position !== undefined) {
            let mediaPosition = entity.attributes.media_position,
                mediaDuration = entity.attributes.media_duration,
                mediaPositionUpdatedAt = entity.attributes.media_position_updated_at;

            function updateTimeLeft() {
              if (entity.state === 'playing') {
                let currentPosition = mediaPosition + ((Date.now() - new Date(mediaPositionUpdatedAt).getTime()) / 1000);
                let timeLeft = mediaDuration - currentPosition;
                let minutes = Math.floor(timeLeft / 60);
                let seconds = Math.floor(timeLeft % 60);
                return `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
              } else {
                let timeLeft = mediaDuration - mediaPosition;
                let minutes = Math.floor(timeLeft / 60);
                let seconds = Math.floor(timeLeft % 60);
                return `${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
              }
            }
            let timeLeftString = updateTimeLeft();

            (function loop() {
              setTimeout(() => {
                timeLeftString = updateTimeLeft();
                loop();
              }, 100);
            })();
            return timeLeftString;
          }
        ]]]

      bar: >
        [[[
          if (entity.attributes.media_position !== undefined) {
          setTimeout(() => {
            let elt = this.shadowRoot,
                card = elt.getElementById('card'),
                container = elt.getElementById('container'),
                bar = elt.getElementById('bar');
            if (elt && card && container && bar) {
                card.insertBefore(bar, container);
                  function update() {
                    let mediaPositionUpdatedAt = entity.attributes.media_position_updated_at,
                        mediaPosition = entity.attributes.media_position,
                        mediaDuration = entity.attributes.media_duration,
                        mediaContentType = entity.attributes.media_content_type;
                    let percentage = entity.state === 'paused'
                      ? (mediaPosition / mediaDuration * 100)
                      : entity.state === 'playing'
                        ? (((Date.now() / 1000) - (new Date(mediaPositionUpdatedAt).getTime() / 1000) + mediaPosition) / mediaDuration * 100)
                        : 0;
                    bar.style.width = percentage.toFixed(1) + '%';
                    requestAnimationFrame(update);
                  }
                  requestAnimationFrame(update);
            }
          }, 0);
          return ' ';}
        ]]]
      progress: >
        [[[
          if (entity.attributes.media_position !== undefined) {
          return ' ';}
        ]]]
      - type: custom:button-card
        template:
          - custom_card_mediaplayer_music
          - progress_bar
        entity: media_player.spotify
        triggers_update: all

CleanShot 2024-07-05 at 18.44.07

2 Likes