Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add long press support #40

Closed
wants to merge 3 commits into from

Conversation

Projects
None yet
10 participants
@Lopton
Copy link

commented Jan 6, 2019

This is to add long press support to the button-card. Changes were made to allow for tracking the length of the user pressing and the x-y coordinates.

If the user pressed for <500 milliseconds it is counted as a single click.

If the user presses for >500 milliseconds it is counted as a long press.

Also tweaked the code to allow for "more-info" or "more_info", since the other HASS standard lovelace items use the "-"

Please note, I'm pretty new at this, so let me know if I did something wrong.

Fixes #55

@Lopton

This comment has been minimized.

Copy link
Author

commented Jan 6, 2019

Addresses issue #35

@andreasnanko

This comment has been minimized.

Copy link

commented Jan 6, 2019

Great work, thank you. It seems to me that it's not possible to have a service for action and for long_press_action, since it's only defined once?

@Lopton

This comment has been minimized.

Copy link
Author

commented Jan 7, 2019

Great point, I'll review that.

@kuuji

This comment has been minimized.

Copy link
Collaborator

commented Jan 7, 2019

Very good point @andreasnanko ! Maybe we could redesign action so that's it's an object instead of just a field? And have the action type + the service under there.

That would break backward compatibility though.

The only way to avoid that ^ would be a dedicated field for long_press service. Seems a bit dirty to me.

Let me know if you have any other ideas.

@thib5

This comment has been minimized.

Copy link

commented Jan 11, 2019

Hi ! where are we with that ? I would really like to have the long press : more info

@andreasnanko

This comment has been minimized.

Copy link

commented Jan 11, 2019

I guess it would be best to redesign the actions. So there would be possibility for adding more action types in the future, like double tapping or whatever, without always adding new dedicated fields.

@thib5 I am using this PR for testing purpose since the thread started, works very good, so enjoy testing if you want too.

@thib5

This comment has been minimized.

Copy link

commented Jan 11, 2019

you said it's working right now ? because it's not working for me ? do I need to add some code to make it work or by default it trigger more info ?

@iantrich

This comment has been minimized.

Copy link
Contributor

commented Jan 17, 2019

I would suggest mirroring what was done in core https://www.home-assistant.io/lovelace/picture-entity/#tap_action

@jonostanck

This comment has been minimized.

Copy link

commented Jan 21, 2019

If anyone wants a dirty approach for a long_press with alternative service call - here is my attempt using Lopton's code and the latest button-card version (one without remote Lit dependency).

var LitElement = LitElement || Object.getPrototypeOf(customElements.get("hui-error-entity-row"));
var html = LitElement.prototype.html;

class ButtonCard extends LitElement {
  static get properties() {
    return {
      hass: Object,
      config: Object,
    };
  }

  render() {
    const state = this.__hass.states[this.config.entity];
    switch (this.config.color_type) {
      case 'blank-card':
        return this.blankCardColoredHtml(state, this.config);
      case 'label-card':
        return this.labelCardColoredHtml(state, this.config);
      case 'card':
        return this.cardColoredHtml(state, this.config);
      case 'icon':
      default:
        return this.iconColoredHtml(state, this.config);
    }
  }


  getFontColorBasedOnBackgroundColor(backgroundColor) {
    const parsedRgbColor= backgroundColor.match(/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/i);
    const parsedBackgroundColor = parsedRgbColor ? parsedRgbColor : this.hexToRgb(backgroundColor.substring(1));
    let fontColor = ''; // don't override by default
    if (parsedBackgroundColor) {
      // Counting the perceptive luminance - human eye favors green color...
      const luminance = (0.299 * parsedBackgroundColor[1] + 0.587 * parsedBackgroundColor[2] + 0.114 * parsedBackgroundColor[3]) / 255;
      if (luminance > 0.5) {
        fontColor = 'rgb(62, 62, 62)'; // bright colors - black font
      } else {
        fontColor = 'rgb(234, 234, 234)';// dark colors - white font
      }
    }
    return fontColor;
  }

  hexToRgb(hex) {
    var bigint = parseInt(hex, 16);
    var r = (bigint >> 16) & 255;
    var g = (bigint >> 8) & 255;
    var b = bigint & 255;

    return [,r,g,b];
  }


  buildCssColorAttribute(state, config) {
    let color = config.color;
    if (state) {
      let configState = config.state ? config.state.find(configState => { return configState.value === state.state; }) : false;
      if(configState){
        color = configState.color ? configState.color : config.color_off;
        if (configState.color === 'auto') {
          color = state.attributes.rgb_color ? `rgb(${state.attributes.rgb_color.join(',')})` : configState.default_color;
        }
      }else{
        if (config.color === 'auto') {
          color = state.attributes.rgb_color ? `rgb(${state.attributes.rgb_color.join(',')})` : config.default_color;
        }
        color = state.state === 'on' ? color : config.color_off;
      }
    }
    return color;
  }

  buildIcon(state, config) {
    let iconOff = config.icon;
    if (config.icon == 'attribute') {
      if (state) {
        const icon = state.attributes.icon;
        return icon;
      }
      return iconOff;
    }
    let configState = config.state ? config.state.find(configState => { return configState.value === state.state; }) : false;
    if (configState && configState.icon) {
      const icon = configState.icon;
      return icon;
    }
    return iconOff;
  }

  blankCardColoredHtml(state, config) {
    const color = this.buildCssColorAttribute(state, config);
    const fontColor = this.getFontColorBasedOnBackgroundColor(color);
    return html`
    <ha-card style="color: ${fontColor}; background-color: ${color}; ${config.card_style}" on-down="${ev => this._down(ev, state, config)}" on-up="${ev => this._up(ev, state, config)}">
    </ha-card>
    `;
  }

  labelCardColoredHtml(state, config) {
    const color = this.buildCssColorAttribute(state, config);
    const fontColor = this.getFontColorBasedOnBackgroundColor(color);
    return html`
    <style>
    ha-icon {  
      display: flex;
      margin: auto;
    }
    paper-button {
      display: flex;
      margin: auto;
      text-align: center;
    }
    </style>
    <ha-card style="color: ${fontColor};">
      <paper-button noink style="background-color: ${color}; ${config.card_style}">
      <div>
        ${config.icon ? html`<ha-icon style="width: ${config.size}; height: ${config.size};" icon="${config.icon}"></ha-icon>` : ''}
        ${config.name ? html`<span>${config.name}</span>` : ''}
       </div>
      </paper-button>
    </ha-card>
    `;
  }

  cardColoredHtml(state, config) {
    const color = this.buildCssColorAttribute(state, config);
    const fontColor = this.getFontColorBasedOnBackgroundColor(color);
    return html`
    <style>
    ha-icon {
      display: flex;
      margin: auto;
    }
    paper-button {
      display: flex;
      margin: auto;
      text-align: center;
    }
    </style>
    <ha-card style="color: ${fontColor};" @down="${ev => this._down(ev, state, config)}" @up="${ev => this._up(ev, state, config)}">
      <paper-button style="background-color: ${color}; ${config.card_style}">
      <div>
        ${config.icon ? html`<ha-icon style="width: ${config.size}; height: ${config.size};" icon="${config.icon}"></ha-icon>` : ''}
        ${config.name ? html`<span>${config.name}</span>` : ''}
        ${config.show_state ? html`<span>${state.state} ${state.attributes.unit_of_measurement ? state.attributes.unit_of_measurement : ''}</span>` : ''}
       </div>
      </paper-button>
    </ha-card>
    `;
  }

  iconColoredHtml(state, config) {
    const color = this.buildCssColorAttribute(state, config);
    const icon = this.buildIcon(state, config);
    return html`
    <style>
    ha-icon {
      display: flex;
      margin: auto;
    }
    paper-button {
      display: flex;
      margin: auto;
      text-align: center;
    }
    </style>
    <ha-card @down="${ev => this._down(ev, state, config)}" @up="${ev => this._up(ev, state, config)}">
      <paper-button style="${config.card_style}">
      <div>
        ${config.icon ? html`<ha-icon style="color: ${color}; width: ${config.size}; height: ${config.size};" icon="${icon}"></ha-icon>` : ''}
        ${config.name ? html`<div>${config.name}</div>` : ''}
        ${config.show_state ? html`<div>${state.state} ${state.attributes.unit_of_measurement ? state.attributes.unit_of_measurement : ''}</div>` : ''}
      </div>
      </paper-button>
    </ha-card>
    `;
  }

  setConfig(config) {
    // if (!config.entity) {
    //   throw new Error('You need to define entity');
    // }
    this.config = {...config};
    this.config.color = config.color ? config.color : 'var(--primary-text-color)';
    this.config.size = config.size ? config.size : '40%';
    let cardStyle = '';
    if (config.style) {
      config.style.forEach((cssObject) => {
        const attribute = Object.keys(cssObject)[0];
        const value = cssObject[attribute];
        cardStyle += `${attribute}: ${value};\n`;
      });
    }
    this.config.color_type = config.color_type ? config.color_type : 'icon';
    this.config.color_off = config.color_off ? config.color_off : 'var(--disabled-text-color)';
    this.config.default_color = config.default_color ? config.default_color : 'var(--primary-text-color)';
    this.config.card_style = cardStyle;
    this.config.name = config.name ? config.name : '';
  }

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

  _down(event) {
    this.cancel = false;
    this.held = false;
    this.xStart = event.detail.x;
    this.yStart = event.detail.y;
    this.timer = window.setTimeout(() => {
        this.held = true;
      }, 500);
  }

  _up(event, state, config) {
    // on all up events clear the timer
    this.timer = undefined;
    // check if the start x and y positions are within 100px of the original (did user drag finger away, if so no hold)
    if (Math.abs(this.xStart - event.detail.x) > 100) {
      this.held = false;
      this.cancel = true;
    } else if (Math.abs(this.yStart - event.detail.y) > 100) {
      this.held = false;
      this.cancel = true;
    }

    if (this.cancel === false) {
      this._toggle(state, config);
    }
  }

  _toggle(state, config) {

    var actionType;
    if (this.held === false) {
      actionType = config.action
    } else {
      actionType = config.long_press_action
    }

    switch (actionType) {
      case 'toggle':
        this.hass.callService('homeassistant', 'toggle', {
          entity_id: state.entity_id,
        });
        break;
      case 'more_info': {
        const node = this.shadowRoot;
        const options = {};
        const detail = { entityId: state.entity_id };
        const event = new Event('hass-more-info', {
          bubbles: options.bubbles === undefined ? true : options.bubbles,
          cancelable: Boolean(options.cancelable),
          composed: options.composed === undefined ? true : options.composed,
        });
        event.detail = detail;
        node.dispatchEvent(event);
        return event;
      }
      case 'service':
        this.hass.callService(config.service.domain, config.service.action, config.service.data);
        break;
      case 'service_secondary':
        this.hass.callService(config.service_secondary.domain, config.service_secondary.action, config.service_secondary.data);
        break;
      default:
        this.hass.callService('homeassistant', 'toggle', {
          entity_id: state.entity_id,
        });
        break;
    }
  }
}

customElements.define('button-card', ButtonCard);

and the way I'm using it:

type: "custom:button-card"
color_type: card
color: rgb(230, 230, 230)
icon: mdi:circle-medium
action: service
service:
  domain: media_player
  action: kodi_call_method
  data:
    entity_id: media_player.kodi
    method: "Input.Select"
long_press_action: service_secondary
service_secondary:
  domain: media_player
  action: kodi_call_method
  data:
    entity_id: media_player.kodi
    method: "Input.ShowOSD"
@iantrich

This comment has been minimized.

Copy link
Contributor

commented Jan 21, 2019

I think this would be the most elegant way to implement this: https://github.com/thomasloven/lovelace-card-tools#longpress

@thomasloven has been creating some awesome tools for custom-card devs. We can all be standardized then and have a common place to resolve issues.

@kuuji

This comment has been minimized.

Copy link
Collaborator

commented Jan 21, 2019

Thanks @iantrich! I couldn't get @ha-click to work this weekend but I'll have a second try with the example from @thomasloven

@thomasloven

This comment has been minimized.

Copy link

commented Jan 21, 2019

It doesn't happen by itself. The element must be registered with the long-press handler.

Also, I think the long press handler must be loaded by something else. I.e. if there are no elements in the view (or in a previously displayed view) which natively supports long-press, the long-press handler won't exist. The odds of that happening are small, though...

@kuuji

This comment has been minimized.

Copy link
Collaborator

commented Jan 21, 2019

Hum that would make sense. My interface contains mostly button cards.

@thib5

This comment has been minimized.

Copy link

commented Jan 21, 2019

Hey ! i'm trying to do something like that :

      - type: custom:card-modder
        card:  
            type: "custom:button-card"
            icon: 'mdi:kodi'
            action: service
            service:
              domain: shell_command
              action: kodi_salon_enter
            long_press_action: service
             service:
                domain: shell_command
                action: kodi_salon_stop

What is the good syntax ?

@jonostanck

This comment has been minimized.

Copy link

commented Jan 21, 2019

@thib5 If you are using my ugly solution which I pasted above then you should change it to:

      - type: custom:card-modder
        card:  
            type: "custom:button-card"
            icon: 'mdi:kodi'
            action: service
            service:
              domain: shell_command
              action: kodi_salon_enter
            long_press_action: service_secondary
             service_secondary:
                domain: shell_command
                action: kodi_salon_stop
@thib5

This comment has been minimized.

Copy link

commented Jan 22, 2019

@jonostanck I'm not sure that I understand your ugly solution ...

@jonostanck

This comment has been minimized.

Copy link

commented Jan 22, 2019

@thib5 You have to change button-card.js to code which I have posted and after this you can use service_secondary in your yaml.

@jimz011

This comment has been minimized.

Copy link

commented Jan 28, 2019

Thank you so much @Lopton and @jonostanck this actually works pretty good. Hopefully this will be merged in the next update. (or something similar to this). Anyways thanks.

btw, updating via custom_updater will overwrite this code right?

@jonostanck

This comment has been minimized.

Copy link

commented Jan 28, 2019

@jimz011 I'm not using custom_updater but I guess it will.

@aptonline aptonline referenced this pull request Feb 21, 2019

Closed

Long tap #55

@postlund

This comment has been minimized.

Copy link

commented Feb 24, 2019

Any more updates here? This feature would be super useful!

@jimz011

This comment has been minimized.

Copy link

commented Feb 25, 2019

I too am looking for this @jonostanck and @Lopton previous code worked fine but it unfortunately broke. I have tried to play around with the code to get it to work with the new button card version (post HA 0.88.x) but I am no coder so it didn’t work out. Hopefully this feature will come to the core button card some day (or even better the core HA entity button).

@thomasloven

This comment has been minimized.

Copy link

commented Feb 25, 2019

(or even better the core HA entity button).

It's always been there...
https://www.home-assistant.io/lovelace/entity-button/#tap_action

@jimz011

This comment has been minimized.

Copy link

commented Feb 25, 2019

@thomasloven I know, I said it wrong. What I meant was that I hope this long press support will come to the button card from kuuji. The reason I said core entity button is because for some reason it doesn’t work for me or very bad. (This only applies to my ios devices though). When I use the core entity button with a long press action it would often not work, or just think it was a tap. It will work eventually, but having to do 4 or more tries to get the more-info pane up isn’t ideal.

Edit: I see that there have been changes to the entity-button card in HA 0.85. It might have fixed the hold action on ios. Going to try it later on. Still wish for this to come to kuuji’s card though.

Edit2: The hold action has been fixed in the core button it seems. Working fine for me now.
Thank you @thomasloven

@thib5

This comment has been minimized.

Copy link

commented Mar 19, 2019

Hey @matt Emerson do you think you can update your code for 0.89 ??THANKS :D

@thib5

This comment has been minimized.

Copy link

commented Mar 26, 2019

EDIT .... SORRY I MEAN @Lopton and @jonostanck

@RomRider

This comment has been minimized.

Copy link
Collaborator

commented Apr 20, 2019

Closing in favor of #106

@RomRider RomRider closed this Apr 20, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.