New version of the plant integration

This explains how to add custom repository in HACS:

I added it to the README.

1 Like

Hi, thanks for the integration and card. I am by no means a CSS expert but I managed to adapt your code to create a space saving small version of your card. It can be used to put two cards side by side. I’m no git expert either but you’re interested I can see if I can create a pull request with the small variant I made. It still has the options to select all attributes, though it is probably at it’s best if you don’t show too many, it is meant to stay small :slight_smile:

IMG_0612

ps. the rounded corners on the card are from the Minimalist theme I use, I did not change the card in that respect.

Can you share this?

Looks fine!
Thanks for the effort.
Is this only a css modification, or did you change something more significant as well?
How do you want to treat it? Should there be an option to the card to use the small or large version? Should it automatically be small if you only add a single sensor? Should it be a different (sister) card?

I made a copy of the original and changed the name so I now have a separate card. Because I’m not that familiar with css it was a bit trial and error and I did not want to mess up the original :slight_smile:

I also made some code changes to remove the divider and the numerical attribute values, and have one attribute list instead of two columns. Also I only show the battery if it is nearing empty. Maybe some of these changes could be done using css alone too.

I think a separate card probably makes most sense. As it’s only a single file I can paste the code here too if you want?

Yes please!

Can’t wait, can you ? :slight_smile: Ok, I this is what’s in my my-small-flower-card.js that you need to add as a lovelace resource:

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


export const fireEvent = (node, type, detail) => {
  detail = detail === null || detail === undefined ? {} : detail;
  const event = new Event(type, {
    bubbles: true,
    cancelable: false,
    composed: true
  });
  event.detail = detail;
  node.dispatchEvent(event);
  return event;
};

const default_show_bars = [
  "moisture"
];

const missingImage = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiIGZvY3VzYWJsZT0iZmFsc2UiIHJvbGU9ImltZyIgYXJpYS1oaWRkZW49InRydWUiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CiAgICAgIDxnPgogICAgICA8IS0tP2xpdCQ0MTM0MjMxNjkkLS0+PHBhdGggZD0iTTMsMTNBOSw5IDAgMCwwIDEyLDIyQzEyLDE3IDcuOTcsMTMgMywxM00xMiw1LjVBMi41LDIuNSAwIDAsMSAxNC41LDhBMi41LDIuNSAwIDAsMSAxMiwxMC41QTIuNSwyLjUgMCAwLDEgOS41LDhBMi41LDIuNSAwIDAsMSAxMiw1LjVNNS42LDEwLjI1QTIuNSwyLjUgMCAwLDAgOC4xLDEyLjc1QzguNjMsMTIuNzUgOS4xMiwxMi41OCA5LjUsMTIuMzFDOS41LDEyLjM3IDkuNSwxMi40MyA5LjUsMTIuNUEyLjUsMi41IDAgMCwwIDEyLDE1QTIuNSwyLjUgMCAwLDAgMTQuNSwxMi41QzE0LjUsMTIuNDMgMTQuNSwxMi4zNyAxNC41LDEyLjMxQzE0Ljg4LDEyLjU4IDE1LjM3LDEyLjc1IDE1LjksMTIuNzVDMTcuMjgsMTIuNzUgMTguNCwxMS42MyAxOC40LDEwLjI1QzE4LjQsOS4yNSAxNy44MSw4LjQgMTYuOTcsOEMxNy44MSw3LjYgMTguNCw2Ljc0IDE4LjQsNS43NUMxOC40LDQuMzcgMTcuMjgsMy4yNSAxNS45LDMuMjVDMTUuMzcsMy4yNSAxNC44OCwzLjQxIDE0LjUsMy42OUMxNC41LDMuNjMgMTQuNSwzLjU2IDE0LjUsMy41QTIuNSwyLjUgMCAwLDAgMTIsMUEyLjUsMi41IDAgMCwwIDkuNSwzLjVDOS41LDMuNTYgOS41LDMuNjMgOS41LDMuNjlDOS4xMiwzLjQxIDguNjMsMy4yNSA4LjEsMy4yNUEyLjUsMi41IDAgMCwwIDUuNiw1Ljc1QzUuNiw2Ljc0IDYuMTksNy42IDcuMDMsOEM2LjE5LDguNCA1LjYsOS4yNSA1LjYsMTAuMjVNMTIsMjJBOSw5IDAgMCwwIDIxLDEzQzE2LDEzIDEyLDE3IDEyLDIyWiI+PC9wYXRoPgogICAgICA8L2c+Cjwvc3ZnPgo=";

class SmallFlowerCard extends LitElement {

  static getConfigElement() {
    return document.createElement("my-small-flower-card-editor");
  }

  static getStubConfig(ha) {
    const supportedEntities = Object.values(ha.states).filter(
      (entity) => entity.entity_id.indexOf('plant.') === 0
    );
    const entity = supportedEntities.length > 0 ? supportedEntities[0].entity_id : 'plant.my_plant';

    return {
      entity: entity,
      battery_sensor: "sensor.myflower_battery",
      show_bars: default_show_bars
    }
  }

  // The user supplied configuration. Throw an exception and Home Assistant
  // will render an error card.
  setConfig(config) {
    if (!config.entity) {
      throw new Error("You need to define an entity");
    }
    this.config = config;
  }
  set hass(hass) {
    this._hass = hass;
    this.stateObj = hass.states[this.config.entity];
    if (!this.prev_fetch) {
      this.prev_fetch = 0;
    }
    // Only fetch once every second at max.  HA is flooeded with websocket requests
    if (Date.now() > this.prev_fetch + 1000) {
      this.prev_fetch = Date.now();
      this.get_data(hass).then(() => {
        this.requestUpdate();
      });
    }
  }

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

  // Use websocket to get plant-data
  async get_data(hass) {
    try {
      this.plantinfo = await hass.callWS({
        type: "plant/get_info",
        entity_id: this.config.entity,
      });
    } catch (err) {}
  }

  moreInfo(entityId = this.config.entity) {
    fireEvent(
      this,
      'hass-more-info',
      {
        entityId,
      },
      {
        bubbles: false,
        composed: true,
      }
    );
  }

  static get styles() {
    return css`
    .attributes {
      white-space: nowrap;
      padding: 1px 0px 0px 4px;
    }
    .attribute ha-icon {
      vertical-align: middle;
      display: inline-grid;
    }
    .attribute {
      display: inline-block;
      width: 100%;
      vertical-align: middle;
      white-space: nowrap;
    }
    #battery {
      float: right;
      margin-right: 8px;
      margin-top: -4px;
    }
    .header {
      padding-top: 4px;
      height: 55px;
    }
    .attribute .header {
      height: auto;
    }
    .header > img {
      border-radius: 50%;
      width: 50px;
      height: 50px;
      object-fit: cover;
      margin-left: 8px;
      margin-right: 8px;
      margin-top: 4px;
      margin-bottom: 0px;
      float: left;
      box-shadow: var( --ha-card-box-shadow, 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2) );
    }
    .header > #name {
      font-weight: bold;
      width: 100%;
      margin-top: 8px;
      display: block;
      white-space: nowrap;
    }
    #name ha-icon {
      color: rgb(240, 163, 163);
    }
    .header > #species {
      text-transform: capitalize;
      line-height: 85%;
      font-size: 0.8em;
      margin-top: 0px;
      margin-right: 4px;
      opacity: 0.4;
      display: block;
    }
    .meter {
      height: 8px;
      background-color: #88888855;
      border-radius: 2px;
      display: inline-grid;
      overflow: hidden;
    }
    .meter.red {
      width: 10%;
    }
    .meter.green {
      width: 50%;
    }
    .meter > span {
      grid-row: 1;
      grid-column: 1;
      height: 100%;
    }
    .meter > .good {
      background-color: rgba(43,194,83,1);
    }
    .meter > .bad {
      background-color: rgba(241,139,130);
    }
    .meter > .unavailable {
      background-color: rgba(158,158,158,1);
    }
    .divider {
      height: 1px;
      background-color: #727272;
      opacity: 0.25;
      margin-left: 8px;
      margin-right: 8px;
    }
    .tooltip {
      position: relative;
    }
    .tooltip .tip {
      opacity: 0;
      visibility: hidden;
      position: absolute;
      padding: 6px 10px;
      top: 3.3em;
      left: 50%;
      -webkit-transform: translateX(-50%) translateY(-180%);
              transform: translateX(-50%) translateY(-180%);
      background: grey;
      color: white;
      white-space: nowrap;
      z-index: 2;
      border-radius: 2px;
      transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), -webkit-transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
      transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
      transition: opacity 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1), -webkit-transform 0.2s cubic-bezier(0.64, 0.09, 0.08, 1);
    }
    .battery.tooltip .tip {
      top: 2em;
    }
    .tooltip:hover .tip, .tooltip:active .tip {
      display: block;
      opacity: 1;
      visibility: visible;
      -webkit-transform: translateX(-50%) translateY(-200%);
              transform: translateX(-50%) translateY(-200%);
    }
    @media (max-width: 600px) {
      .header > .unit {
        display: none;
      }
    }
    `;
  }

  render() {
    if (
      !this.config ||
      !this._hass
    ) {
      // console.log(this.config);
      // console.log(this.stateObj);
      // console.log(this._hass);
      return null;
    }
    // console.log(this.config);
    // console.log(this._hass);
    // const stateObj = this.hass.states[this.config.entity];
    if (!this.stateObj) {
      return html`
          <hui-warning>
          Entity not available: ${this.config.entity}
          </hui-warning>
        `;
    }
    const species = this.stateObj.attributes.species;
    var icons = {};
    var uom = {};
    var uomt = {};
    var limits = {};
    var curr = {};
    var sensors = {};
    var displayed = [];
    var monitored = this.config.show_bars || default_show_bars;
    const battery_sensor = this.config.battery_sensor || null;

    if (this.plantinfo && this.plantinfo["result"]) {
      const result = this.plantinfo["result"];
      for (var i = 0; i < monitored.length; i++) {
        let elem = monitored[i];
        if (result[elem]) {
          limits["max_" + elem] = result[elem].max;
          limits["min_" + elem] = result[elem].min;
          curr[elem] = result[elem].current;
          icons[elem] = result[elem].icon;
          sensors[elem] = result[elem].sensor;
          uomt[elem] = result[elem].unit_of_measurement;
          uom[elem] = result[elem].unit_of_measurement;
          if (elem == "dli") {
            uomt["dli"] = "mol/d⋅m²";
            uom["dli"] = '<math style="display: inline-grid;" xmlns="http://www.w3.org/1998/Math/MathML"><mrow><mfrac><mrow><mn>mol</mn></mrow><mrow><mn>d</mn><mn>⋅</mn><msup><mn>m</mn><mn>2</mn></msup></mrow></mfrac></mrow></math>';
          }
          displayed.push(elem);
        }
      }
    }
    const attribute = (attr) => {
      const min = parseFloat(limits["min_" + attr]);
      const max = parseFloat(limits["max_" + attr]);
      const unit = uom[attr];
      // console.log(html`${unsafeHTML(unit)}`);
      const unitTooltip = uomt[attr];
      const icon = icons[attr] || "mdi:help-circle-outline";
      var val = parseFloat(curr[attr]);
      if (isNaN(val)) {
        var aval = false;
        var pct = 0;
        val = "";
      } else {
        var aval = true;
        var pct = 100 * Math.max(0, Math.min(1, (val - min) / (max - min)));
      }

      var toolTipText = aval ? attr + ": " + val + " " + unitTooltip + '<br>(' + min + " ~ " + max +" " + unitTooltip + ")"
                             : this._hass.localize('state.default.unavailable');

      return html`
      <div class="attribute tooltip" @click="${() => this.moreInfo(sensors[attr])}">
        <div class="tip" style="text-align:center;">${unsafeHTML(toolTipText)}</div>
        <ha-icon .icon="${icon}"></ha-icon>
        <div class="meter red">
          <span class="${
            aval ? (val < min || val > max ? "bad" : "good") : "unavailable"
          }" style="width: 100%;"></span>
        </div>
        <div class="meter green">
          <span class="${
            aval ? (val > max ? "bad" : "good") : "unavailable"
          }" style="width:${aval ? pct : "0"}%;"></span>
        </div>
        <div class="meter red">
          <span class="bad" style="width:${
            aval ? (val > max ? 100 : 0) : "0"
          }%;"></span>
      `;
    };
    const battery = () => {
      if (battery_sensor) {
        if (this._hass.states[battery_sensor]) {
          var value = this._hass.states[battery_sensor].state + '%';
          switch (true) {
            case this._hass.states[battery_sensor].state > 20:
              return html``;
            case this._hass.states[battery_sensor].state > 10:
              var icon = "mdi:battery-20";
              var battery_color = "red";
              break;
            case this._hass.states[battery_sensor].state > 0:
              var icon = "mdi:battery-10";
              var battery_color = "red";
              break;
            case this._hass.states[battery_sensor].state == 0:
              var icon = "mdi:battery-alert-variant-outline";
              var battery_color = "red";
              break;
            default:
              var icon = "mdi:battery-off-outline";
              var battery_color = "rgba(158,158,158,1)";
              var value =  this._hass.localize('state.default.unavailable');
              break;
          }
        } else {
          var icon = "mdi:battery-off-outline";
          var battery_color = "rgba(158,158,158,1)";
          var value =  this._hass.localize('state.default.unavailable');
        }
        return html`
        <div class="battery tooltip">
        <div class="tip" style="text-align:center;">${value}</div>
        <ha-icon .icon="${icon}" style="color: ${battery_color}"></ha-icon>
        </div>
        `;
      } else {
        return html``;
      }
    };
    return html`
      <ha-card>
      <div class="header" @click="${() =>
        this.moreInfo(this.stateObj.entity_id)}">
        <img src="${
          this.stateObj.attributes.entity_picture
            ? this.stateObj.attributes.entity_picture
            : missingImage
        }">
        <span id="name"> ${
          this.stateObj.attributes.friendly_name
        } <ha-icon .icon="mdi:${
      this.stateObj.state.toLowerCase() == "problem"
        ? "alert-circle-outline"
        : ""
    }"></ha-icon>
        </span>
        <span id="battery">${battery()}</span>
        <span id="species">${species}</span>
      </div>
      <div class="attributes">
        ${displayed[0] == undefined ? void 0 : attribute(displayed[0])}
      </div>
      <div class="attributes">
          ${displayed[1] == undefined ? void 0 : attribute(displayed[1])}
      </div>
      <div class="attributes">
        ${displayed[2] == undefined ? void 0 : attribute(displayed[2])}
      </div>
      <div class="attributes">
          ${displayed[3] == undefined ? void 0 : attribute(displayed[3])}
      </div>
      <div class="attributes">
        ${displayed[4] == undefined ? void 0 : attribute(displayed[4])}
      </div>
      <div class="attributes">
          ${displayed[5] == undefined ? void 0 : attribute(displayed[5])}
      </div>
      </ha-card>
      `;
  }
}

class SmallContentCardEditor extends LitElement {

  static get properties() {
    return {
      hass: {},
      _config: {},
    };
  }
  setConfig(config) {
    this._config = config;
  }
  set hass(hass) {
    this._hass = hass;
  }

  _valueChanged(ev) {
    // console.log("ValueChanged");
    // console.log(ev);
    if (!this._config || !this._hass) {
      return;
    }
    const _config = Object.assign({}, this._config);
    _config.entity = ev.detail.value.entity;
    _config.battery_sensor = ev.detail.value.battery_sensor;
    _config.show_bars = ev.detail.value.show_bars;

    this._config = _config;

    const event = new CustomEvent("config-changed", {
      detail: { config: _config },
      bubbles: true,
      composed: true,
    });
    this.dispatchEvent(event);
  }

  render() {
    // console.log("Render");
    // console.log(this._config);
    if (!this._hass || !this._config) {
      return html``;
    }
    if (!this._config.hasOwnProperty('show_bars')) {
      // Enable all bars by default
      this._config.show_bars = default_show_bars;
    }
    return html`
      <ha-form
      .hass=${this._hass}
      .data=${this._config}
      .schema=${[
        {name: "entity", selector: { entity: { domain: "plant" } }},
        {name: "battery_sensor", selector: { entity: { device_class: "battery" } }},
        {name: "show_bars", selector: { select: { multiple: true, mode: "list", options: [
	  {label: "Moisture", value: "moisture"},
	  {label: "Conductivity", value: "conductivity"},
	  {label: "Temperature", value: "temperature"},
	  {label: "Illuminance", value: "illuminance"},
	  {label: "Humidity", value: "humidity"},
	  {label: "Daily Light Integral", value: "dli"}
	  ]}
	}}
      ]}
      .computeLabel=${this._computeLabel}
      @value-changed=${this._valueChanged} 
      ></ha-form>
    `;
  }
}


customElements.define("my-small-flower-card", SmallFlowerCard);
customElements.define("my-small-flower-card-editor", SmallContentCardEditor);
window.customCards = window.customCards || [];
window.customCards.push({
    type: "my-small-flower-card",
    name: "Small Flower Card",
    preview: true, // Optional - defaults to false
    description: "Custom flower card for https://github.com/Olen/homeassistant-plant", // Optional
});

1 Like

If you have your own github account I can help you with what you need to get the hacs-infrastructure up. And link to your card.

If you don’t want to do it yourself, I can probably add it as a repo myself, unless someone else wants to host it.

I’m gonna try this hopefully on Friday


Loving this :slight_smile: made a few tweaks with the width of the bar

1 Like

Would be really cool if you could share the code

It’s the same as Edwin posted, but the width of the green class up to 69%

1 Like

Thanks again for the new card version. I can definitely see it’s use.

I made an attempt to merge the two cards, and have a selector in the config, but it did not work out. HA is too good at caching the CSS, so swithcing between modes just did not work.
But on the other hand, I really don’t like to have two cards that share ~80% of the code as two different cards, so I’m not sure what to do…

As long as you use different classes for the big and small card you are ok right? Caching css doesn’t matter then.

Yeah. But it would need a complete refactoring of the code, splitting out common functions to other classes etc. Not that that is a bad thing, it’s quite messy anyway, I just don’t feel the urge to start on that atm.
And since javascript is really not my priamry language, it takes a whole lot of trial and error just to get the basic stuff in these cards working…

But I am more than happy to accept PRs

I could help you, but not before the weekend. I cant promise that I’ll be able to help you this weekend tho.

I would convert to typescript to start and make everything easier. But I guess there is no rush right?

1 Like

This is a great integration. One problem I’m seeing is I’m trying this automation to alert me for watering / feeding requirements but it’s just returning ‘null’ - I’m unsure if this is a config issue my end or due to changes in this plant integration. Anyone tried this, or anything similar? Thanks.

Trace shows…

"trace": {
    "last_step": null,
    "run_id": "69f9b10164bd6150debd699587158168",
    "state": "stopped",
    "script_execution": null,
    "timestamp": {
      "start": "2023-09-21T21:34:00.420133+00:00",
      "finish": "2023-09-21T21:34:00.423693+00:00"
    },
    "domain": "automation",
    "item_id": "1693836426254",
    "error": "UndefinedError: list object has no element 6",

Ah… fixed it. I missed the requirement to have the Forecast Hourly sensor enabled. All good now.

@Olen Converted to typescript and add mini card by marcokreeft87 · Pull Request #47 · Olen/lovelace-flower-card · GitHub check out this PR