diff --git a/.devcontainer/ui-lovelace.yaml b/.devcontainer/ui-lovelace.yaml index fe43aa9..c6e49e9 100644 --- a/.devcontainer/ui-lovelace.yaml +++ b/.devcontainer/ui-lovelace.yaml @@ -80,6 +80,15 @@ views: name: AVG curve: smooth type: line + as_duration: millisecond + group_by: + duration: 10min + func: avg + - entity: sensor.random0_100 + name: AVG + curve: smooth + type: line + as_duration: minute group_by: duration: 10min func: avg @@ -87,6 +96,7 @@ views: curve: smooth name: MIN type: line + as_duration: hour group_by: duration: 10min func: min @@ -94,6 +104,7 @@ views: curve: smooth name: MAX type: line + as_duration: day group_by: duration: 10min func: max @@ -101,6 +112,7 @@ views: curve: smooth name: LAST type: line + as_duration: month group_by: duration: 10min func: last @@ -108,6 +120,7 @@ views: curve: smooth name: FIRST type: line + as_duration: year group_by: duration: 10min func: first diff --git a/README.md b/README.md index 9f898b3..2c9630e 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ The card stricly validates all the options available (but not for the `apex_conf | `invert` | boolean | `false` | v1.2.0 | Negates the data (`1` -> `-1`). Usefull to display opposites values like network in (standard)/out (inverted) | | `data_generator` | string | | v1.2.0 | See [data_generator](#data_generator-option) | | `offset` | string | | NEXT_VERSION | This is different from the main `offset` parameter. This is at the series level. It is only usefull if you want to display data from for eg. yesterday on top of the data from today for the same sensor and compare the data. The time displayed in the tooltip will be wrong as will the x axis information. Valid values are any negative time string, eg: `-1h`, `-12min`, `-1d`, `-1h25`, `-10sec`, ... | +| `to_duration` | string | | NEXT_VERSION | Will pretty print the states as durations. Doesn't affect the graph, only the tooltip/legend/header display. You provide the source unit of your sensor. Valid values are `millisecond`, `second`, `minute`, `hour`, `day`, `week`, `month`, `year`.
Eg: if the state is `345` and `to_duration` is set to `minute` then it would display `5h45m` | ### `show` Options diff --git a/package-lock.json b/package-lock.json index 6b49315..9b78496 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,7 @@ "requires": true, "packages": { "": { - "version": "1.1.0-dev.1", + "version": "1.3.0-dev.2", "license": "MIT", "dependencies": { "@ctrl/tinycolor": "^3.3.3", @@ -17,6 +17,7 @@ "localforage": "^1.9.0", "lz-string": "^1.4.4", "moment": "^2.29.1", + "moment-duration-format": "^2.3.2", "moment-range": "^4.0.2", "parse-duration": "^0.4.4", "spark-md5": "^3.0.1", @@ -36,6 +37,7 @@ "@semantic-release/npm": "^7.0.10", "@semantic-release/release-notes-generator": "^9.0.1", "@types/lz-string": "^1.3.34", + "@types/moment-duration-format": "^2.2.2", "@types/spark-md5": "^3.0.2", "@typescript-eslint/eslint-plugin": "^4.14.0", "@typescript-eslint/parser": "^4.14.0", @@ -1021,6 +1023,15 @@ "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, + "node_modules/@types/moment-duration-format": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/moment-duration-format/-/moment-duration-format-2.2.2.tgz", + "integrity": "sha512-CuYswsMI3y5uR5sD9i/VUqIbZrsYN2eaCs7nH3qpDl2CZlNI48mjMf4ve2RpQ/65irprtnQVetfnea9my+jqcg==", + "dev": true, + "dependencies": { + "moment": ">=2.14.0" + } + }, "node_modules/@types/node": { "version": "14.14.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", @@ -3642,6 +3653,11 @@ "node": "*" } }, + "node_modules/moment-duration-format": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-2.3.2.tgz", + "integrity": "sha512-cBMXjSW+fjOb4tyaVHuaVE/A5TqkukDWiOfxxAjY+PEqmmBQlLwn+8OzwPiG3brouXKY5Un4pBjAeB6UToXHaQ==" + }, "node_modules/moment-range": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/moment-range/-/moment-range-4.0.2.tgz", @@ -12122,6 +12138,15 @@ "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, + "@types/moment-duration-format": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/moment-duration-format/-/moment-duration-format-2.2.2.tgz", + "integrity": "sha512-CuYswsMI3y5uR5sD9i/VUqIbZrsYN2eaCs7nH3qpDl2CZlNI48mjMf4ve2RpQ/65irprtnQVetfnea9my+jqcg==", + "dev": true, + "requires": { + "moment": ">=2.14.0" + } + }, "@types/node": { "version": "14.14.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", @@ -14146,6 +14171,11 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, + "moment-duration-format": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/moment-duration-format/-/moment-duration-format-2.3.2.tgz", + "integrity": "sha512-cBMXjSW+fjOb4tyaVHuaVE/A5TqkukDWiOfxxAjY+PEqmmBQlLwn+8OzwPiG3brouXKY5Un4pBjAeB6UToXHaQ==" + }, "moment-range": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/moment-range/-/moment-range-4.0.2.tgz", diff --git a/package.json b/package.json index 6e8625b..8138dec 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "localforage": "^1.9.0", "lz-string": "^1.4.4", "moment": "^2.29.1", + "moment-duration-format": "^2.3.2", "moment-range": "^4.0.2", "parse-duration": "^0.4.4", "spark-md5": "^3.0.1", @@ -57,6 +58,7 @@ "@semantic-release/npm": "^7.0.10", "@semantic-release/release-notes-generator": "^9.0.1", "@types/lz-string": "^1.3.34", + "@types/moment-duration-format": "^2.2.2", "@types/spark-md5": "^3.0.2", "@typescript-eslint/eslint-plugin": "^4.14.0", "@typescript-eslint/parser": "^4.14.0", diff --git a/src/apex-layouts.ts b/src/apex-layouts.ts index c4b152f..714897c 100644 --- a/src/apex-layouts.ts +++ b/src/apex-layouts.ts @@ -1,8 +1,8 @@ import { HomeAssistant } from 'custom-card-helpers'; import parse from 'parse-duration'; -import { DEFAULT_FLOAT_PRECISION, HOUR_24, moment } from './const'; +import { DEFAULT_FLOAT_PRECISION, HOUR_24, moment, NO_VALUE } from './const'; import { ChartCardConfig } from './types'; -import { computeName, computeUom, mergeDeep } from './utils'; +import { computeName, computeUom, mergeDeep, prettyPrintTime } from './utils'; export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | undefined = undefined): unknown { const def = { @@ -59,7 +59,12 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u }, y: { formatter: function (value, opts, conf = config, hass2 = hass) { - if (value !== null && typeof value === 'number' && !Number.isInteger(value)) { + if ( + value !== null && + typeof value === 'number' && + !Number.isInteger(value) && + !conf.series[opts.seriesIndex]?.as_duration + ) { value = (value as number).toFixed( conf.series[opts.seriesIndex].float_precision === undefined ? DEFAULT_FLOAT_PRECISION @@ -72,7 +77,10 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u undefined, hass2?.states[conf.series[opts.seriesIndex].entity], ); - return [`${value} ${uom}`]; + return conf.series[opts.seriesIndex]?.as_duration + ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + [`${prettyPrintTime(value, conf.series[opts.seriesIndex].as_duration!)}`] + : [`${value} ${uom}`]; }, }, fixed: { @@ -95,7 +103,12 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u const name = computeName(opts.seriesIndex, conf, undefined, hass2?.states[conf.series[opts.seriesIndex].entity]) + ':'; let value = opts.w.globals.series[opts.seriesIndex].slice(-1)[0]; - if (value !== null && typeof value === 'number' && !Number.isInteger(value)) { + if ( + value !== null && + typeof value === 'number' && + !Number.isInteger(value) && + !conf.series[opts.seriesIndex]?.as_duration + ) { value = (value as number).toFixed( conf.series[opts.seriesIndex].float_precision === undefined ? DEFAULT_FLOAT_PRECISION @@ -103,7 +116,18 @@ export function getLayoutConfig(config: ChartCardConfig, hass: HomeAssistant | u ); } const uom = computeUom(opts.seriesIndex, conf, undefined, hass2?.states[conf.series[opts.seriesIndex].entity]); - return [name, value === undefined ? `N/A ${uom}` : `${value} ${uom}`]; + let valueString = ''; + if (value === undefined || value === null) { + valueString = `${NO_VALUE} ${uom}`; + } else { + if (conf.series[opts.seriesIndex]?.as_duration) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + valueString = `${prettyPrintTime(value, conf.series[opts.seriesIndex].as_duration!)}`; + } else { + valueString = `${value} ${uom}`; + } + } + return [name, valueString]; }, }, stroke: { diff --git a/src/apexcharts-card.ts b/src/apexcharts-card.ts index 0eb3b1b..124ab44 100644 --- a/src/apexcharts-card.ts +++ b/src/apexcharts-card.ts @@ -12,6 +12,7 @@ import { log, mergeDeep, offsetData, + prettyPrintTime, validateInterval, validateOffset, } from './utils'; @@ -23,7 +24,7 @@ import GraphEntry from './graphEntry'; import { createCheckers } from 'ts-interface-checker'; import { ChartCardExternalConfig } from './types-config'; import exportedTypeSuite from './types-config-ti'; -import { DEFAULT_FLOAT_PRECISION, moment } from './const'; +import { DEFAULT_FLOAT_PRECISION, moment, NO_VALUE } from './const'; import { DEFAULT_COLORS, DEFAULT_DURATION, @@ -314,7 +315,7 @@ class ChartsCard extends LitElement { private _renderStates(): TemplateResult { return html`
- ${this._config?.series.map((_, index) => { + ${this._config?.series.map((serie, index) => { return html`
@@ -323,7 +324,11 @@ class ChartsCard extends LitElement { style="${this._config?.header?.colorize_states && this._colors && this._colors.length > 0 ? `color: ${this._colors[index % this._colors?.length]};` : ''}" - >${this._lastState?.[index] === 0 ? 0 : this._lastState?.[index] || 'N/A'}${this._lastState?.[index] === 0 + ? 0 + : (serie.as_duration + ? prettyPrintTime(this._lastState?.[index], serie.as_duration) + : this._lastState?.[index]) || NO_VALUE} ${computeUom(index, this._config, this._entities)}
diff --git a/src/const.ts b/src/const.ts index f43d09e..80154eb 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,6 +1,8 @@ import Moment from 'moment'; import { extendMoment } from 'moment-range'; +import momentDurationFormatSetup from 'moment-duration-format'; +momentDurationFormatSetup(Moment); export const moment = extendMoment(Moment); export const ONE_HOUR = 1000 * 3600; export const HOUR_24 = ONE_HOUR * 24; @@ -28,3 +30,5 @@ export const DEFAULT_COLORS = [ '#2980b9', '#8e44ad', ]; + +export const NO_VALUE = 'N/A'; diff --git a/src/types-config-ti.ts b/src/types-config-ti.ts index 8f278b8..b4a0f27 100644 --- a/src/types-config-ti.ts +++ b/src/types-config-ti.ts @@ -40,6 +40,7 @@ export const ChartCardSeriesExternalConfig = t.iface([], { "data_generator": t.opt("string"), "float_precision": t.opt("number"), "offset": t.opt("string"), + "as_duration": t.opt("ChartCardPrettyTime"), "group_by": t.opt(t.iface([], { "duration": t.opt("string"), "func": t.opt("GroupByFunc"), @@ -47,6 +48,8 @@ export const ChartCardSeriesExternalConfig = t.iface([], { })), }); +export const ChartCardPrettyTime = t.union(t.lit('millisecond'), t.lit('second'), t.lit('minute'), t.lit('hour'), t.lit('day'), t.lit('week'), t.lit('month'), t.lit('year')); + export const GroupByFill = t.union(t.lit('null'), t.lit('last'), t.lit('zero')); export const GroupByFunc = t.union(t.lit('raw'), t.lit('avg'), t.lit('min'), t.lit('max'), t.lit('last'), t.lit('first'), t.lit('sum'), t.lit('median'), t.lit('delta')); @@ -63,6 +66,7 @@ const exportedTypeSuite: t.ITypeSuite = { ChartCardExternalConfig, ChartCardSpanExtConfig, ChartCardSeriesExternalConfig, + ChartCardPrettyTime, GroupByFill, GroupByFunc, ChartCardHeaderExternalConfig, diff --git a/src/types-config.ts b/src/types-config.ts index 7ccc48f..cdc3a87 100644 --- a/src/types-config.ts +++ b/src/types-config.ts @@ -36,6 +36,7 @@ export interface ChartCardSeriesExternalConfig { data_generator?: string; float_precision?: number; offset?: string; + as_duration?: ChartCardPrettyTime; group_by?: { duration?: string; func?: GroupByFunc; @@ -43,6 +44,8 @@ export interface ChartCardSeriesExternalConfig { }; } +export type ChartCardPrettyTime = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'; + export type GroupByFill = 'null' | 'last' | 'zero'; export type GroupByFunc = 'raw' | 'avg' | 'min' | 'max' | 'last' | 'first' | 'sum' | 'median' | 'delta'; diff --git a/src/utils.ts b/src/utils.ts index a131bdb..2c21af4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,8 @@ import { compress as lzStringCompress, decompress as lzStringDecompress } from ' import { ChartCardConfig, EntityCachePoints } from './types'; import { TinyColor } from '@ctrl/tinycolor'; import parse from 'parse-duration'; +import { ChartCardPrettyTime } from './types-config'; +import { moment, NO_VALUE } from './const'; export function compress(data: unknown): string { return lzStringCompress(JSON.stringify(data)); @@ -135,3 +137,8 @@ export function offsetData(data: EntityCachePoints, offset: number | undefined): } return data; } + +export function prettyPrintTime(value: string | number | null, unit: ChartCardPrettyTime): string { + if (value === null) return NO_VALUE; + return moment.duration(value, unit).format('y [y] d[d] h[h] m[m] s[s] S[ms]', { trim: 'both' }); +}