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

feat(graph): add graph card #454

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .hass_dev/lovelace-mushroom-showcase.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ views:
- !include views/media-player-view.yaml
- !include views/vacuum-view.yaml
- !include views/lock-view.yaml
- !include views/humidifier-view.yaml
- !include views/humidifier-view.yaml
- !include views/graph-view.yaml
29 changes: 29 additions & 0 deletions .hass_dev/views/graph-view.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
title: Graph
icon: mdi:chart-areaspline
cards:
- type: grid
title: Graph
columns: 2
square: false
cards:
- type: custom:mushroom-graph-card
entity: sensor.outside_temperature
- type: custom:mushroom-graph-card
entity: sensor.outside_temperature
name: Temperature
- type: custom:mushroom-graph-card
entity: sensor.outside_temperature
icon: mdi:chart-areaspline
- type: custom:mushroom-graph-card
entity: sensor.outside_temperature
graph_color: pink
- type: horizontal-stack
title: Display mode
cards:
- type: custom:mushroom-graph-card
entity: sensor.outside_temperature
display_mode: standard
- type: custom:mushroom-graph-card
entity: sensor.outside_temperature
display_mode: compact
square: false
27 changes: 27 additions & 0 deletions docs/cards/graph.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Graph card

![Graph light](../images/graph-light.png)
![Graph dark](../images/graph-dark.png)

## Description

A graph card allow you to show the actual value and the history.

## Configuration variables

All the options are available in the lovelace editor but you can use `yaml` if you want.

| Name | Type | Default | Description |
| :------------------ | :-------------------------------------------------- | :---------- | :--------------------------------------------- |
| `entity` | string | Required | Entity |
| `name` | string | Optional | Custom name |
| `icon` | string | Optional | Custom icon |
| `hours_to_show` | number | Optional | Number of hours to show in the history |
| `graph_color` | string | Optional | Entity chart and icon color |
| `primary_info` | `name` `state` `last-changed` `last-updated` `none` | `name` | Info to show as primary info |
| `secondary_info` | `name` `state` `last-changed` `last-updated` `none` | `state` | Info to show as secondary info |
| `graph_mode` | string | Optional | Mode `line` or `fill` |
| `display_mode` | string | Optional | Mode `standard` or `compact` |
| `tap_action` | action | `more-info` | Home assistant action to perform on tap |
| `hold_action` | action | `more-info` | Home assistant action to perform on hold |
| `double_tap_action` | action | `more-info` | Home assistant action to perform on double_tap |
Binary file added docs/images/graph-dark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/graph-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions src/cards/graph-card/const.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { PREFIX_NAME } from "../../const";

export const GRAPH_CARD_NAME = `${PREFIX_NAME}-graph-card`;
export const GRAPH_CARD_EDITOR_NAME = `${GRAPH_CARD_NAME}-editor`;
export const GRAPH_ENTITY_DOMAINS = ["counter", "input_number", "number", "sensor"];

export const GRAPH_MODE = ["line", "fill"] as const;
export const DISPLAY_MODE = ["standard", "compact"] as const;

export const GRAPH_MINUTE = 60000;
export const GRAPH_HOUR = GRAPH_MINUTE * 60;
export const GRAPH_DEFAULT_HOURS = 24;

export const GRAPH_HEIGHT_STANDARD_MARGIN = 20;
export const GRAPH_HEIGHT_COMPACT_MARGIN = 10;
export const GRAPH_HEIGHT_STANDARD = 80;
export const GRAPH_HEIGHT_COMPACT = 40;
42 changes: 42 additions & 0 deletions src/cards/graph-card/graph-card-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { ActionConfig, LovelaceCardConfig } from "custom-card-helpers";
import { assign, enums, integer, object, optional, string } from "superstruct";
import { actionConfigStruct } from "../../utils/action-struct";
import { baseLovelaceCardConfig } from "../../utils/editor-styles";
import { Info, INFOS } from "../../utils/info";
import { DISPLAY_MODE, GRAPH_MODE } from "./const";

export type GraphMode = typeof GRAPH_MODE[number];
export type DisplayMode = typeof DISPLAY_MODE[number];

export interface GraphCardConfig extends LovelaceCardConfig {
entity?: string;
name?: string;
icon?: string;
hours_to_show?: number;
graph_color?: string;
primary_info?: Info;
secondary_info?: Info;
graph_mode?: GraphMode;
display_mode?: DisplayMode;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
}

export const graphCardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
icon: optional(string()),
name: optional(string()),
hours_to_show: optional(integer()),
graph_color: optional(string()),
primary_info: optional(enums(INFOS)),
secondary_info: optional(enums(INFOS)),
graph_mode: optional(enums(GRAPH_MODE)),
display_mode: optional(enums(DISPLAY_MODE)),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
double_tap_action: optional(actionConfigStruct),
})
);
163 changes: 163 additions & 0 deletions src/cards/graph-card/graph-card-editor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import {
fireEvent,
HomeAssistant,
LocalizeFunc,
LovelaceCardEditor,
stateIcon,
} from "custom-card-helpers";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import memoizeOne from "memoize-one";
import setupCustomlocalize from "../../localize";
import { Action } from "../../utils/form/custom/ha-selector-mushroom-action";
import { HaFormSchema } from "../../utils/form/ha-form";
import { loadHaComponents } from "../../utils/loader";
import {
DISPLAY_MODE,
GRAPH_CARD_EDITOR_NAME,
GRAPH_DEFAULT_HOURS,
GRAPH_ENTITY_DOMAINS,
GRAPH_MODE,
} from "./const";
import { GraphCardConfig, graphCardConfigStruct } from "./graph-card-config";
import { assert } from "superstruct";
import { SelectOption } from "../../utils/form/ha-selector";
import { GENERIC_LABELS } from "../../utils/form/generic-fields";
import { MushroomBaseElement } from "../../utils/base-element";

const actions: Action[] = ["more-info", "call-service", "none"];
const GRAPH_LABELS = ["graph_mode", "display_mode"];

const computeSchema = memoizeOne((localize: LocalizeFunc, icon?: string): HaFormSchema[] => [
{ name: "entity", selector: { entity: { domain: GRAPH_ENTITY_DOMAINS } } },
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "icon", selector: { icon: { placeholder: icon } } },
],
},

{
type: "grid",
name: "",
schema: [
{ name: "primary_info", selector: { "mush-info": {} } },
{ name: "secondary_info", selector: { "mush-info": {} } },
],
},
{
type: "grid",
name: "",
schema: [
{ name: "graph_color", selector: { "mush-color": {} } },
{
name: "hours_to_show",
selector: { number: { min: 1, max: 168, mode: "box", step: 1 } },
},
],
},
{
type: "grid",
name: "",
schema: [
{
name: "graph_mode",
selector: {
select: {
options: GRAPH_MODE.map(
(mode) =>
<SelectOption>{
value: mode,
label:
localize(`editor.card.graph.graph_mode_type.${mode}`) ||
mode,
}
) as SelectOption[],
mode: "dropdown",
},
},
},
{
name: "display_mode",
selector: {
select: {
options: DISPLAY_MODE.map(
(mode) =>
<SelectOption>{
value: `${mode}`,
label:
localize(`editor.card.graph.display_mode_type.${mode}`) ||
mode,
}
) as SelectOption[],
mode: "dropdown",
},
},
},
],
},
{ name: "tap_action", selector: { "mush-action": { actions } } },
{ name: "hold_action", selector: { "mush-action": { actions } } },
{ name: "double_tap_action", selector: { "mush-action": { actions } } },
]);

@customElement(GRAPH_CARD_EDITOR_NAME)
export class GraphCardEditor extends MushroomBaseElement implements LovelaceCardEditor {
@state() private _config?: GraphCardConfig;

connectedCallback() {
super.connectedCallback();
void loadHaComponents();
}

public setConfig(config: GraphCardConfig): void {
assert(config, graphCardConfigStruct);
this._config = config;
}

private _computeLabelCallback = (schema: HaFormSchema) => {
const customLocalize = setupCustomlocalize(this.hass!);

if (GENERIC_LABELS.includes(schema.name)) {
return customLocalize(`editor.card.generic.${schema.name}`);
}

if (GRAPH_LABELS.includes(schema.name)) {
return customLocalize(`editor.card.graph.${schema.name}`);
}
return this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
};

protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}

const entityState = this._config.entity ? this.hass.states[this._config.entity] : undefined;
const entityIcon = entityState ? stateIcon(entityState) : undefined;
const icon = this._config.icon || entityIcon;
const customLocalize = setupCustomlocalize(this.hass!);
const schema = computeSchema(customLocalize, icon);

this._config = {
hours_to_show: GRAPH_DEFAULT_HOURS,
...this._config,
};

return html`
<ha-form
.hass=${this.hass}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}

private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
}
Loading