From 1cb6bb5d0c5abbe73a4e5f0ded426cc123af515b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20W?= Date: Wed, 3 Feb 2021 20:47:13 +0100 Subject: [PATCH] feat(series): `transform` the data the way you want (#45) * New `transform` option in series * Update Roadmap --- .devcontainer/configuration.yaml | 16 ++++++---- .devcontainer/ui-lovelace.yaml | 7 +++++ README.md | 49 ++++++++++++++++++++++++++++-- docs/data_processing_chart.drawio | 1 + docs/data_processing_chart.png | Bin 0 -> 31479 bytes src/graphEntry.ts | 31 +++++++++++-------- src/types-config-ti.ts | 1 + src/types-config.ts | 1 + 8 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 docs/data_processing_chart.drawio create mode 100644 docs/data_processing_chart.png diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 1b0cd7b..d65968f 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -12,18 +12,18 @@ sensor: - platform: template sensors: pressure: - friendly_name: "Pressure" - unit_of_measurement: "hPa" + friendly_name: 'Pressure' + unit_of_measurement: 'hPa' value_template: "{{ state_attr('weather.home', 'pressure') }}" device_class: pressure temperature: - friendly_name: "Temperature" - unit_of_measurement: "°C" + friendly_name: 'Temperature' + unit_of_measurement: '°C' value_template: "{{ state_attr('weather.home', 'temperature') }}" device_class: temperature humidity: - friendly_name: "Humidity" - unit_of_measurement: "%" + friendly_name: 'Humidity' + unit_of_measurement: '%' value_template: "{{ state_attr('weather.home', 'humidity') }}" device_class: humidity - platform: random @@ -38,3 +38,7 @@ sensor: name: random_0_1000 minimum: 0 maximum: 1000 + +input_boolean: + test_boolean: + name: Test Input Boolean diff --git a/.devcontainer/ui-lovelace.yaml b/.devcontainer/ui-lovelace.yaml index 10206b0..b6ca954 100644 --- a/.devcontainer/ui-lovelace.yaml +++ b/.devcontainer/ui-lovelace.yaml @@ -394,3 +394,10 @@ views: - entity: sensor.outside_temperature curve: stepline extend_to_end: false + + - type: custom:apexcharts-card + update_delay: 3s + graph_span: 5min + series: + - entity: input_boolean.test_boolean + transform: "return x === 'on' ? 1 : 0;" diff --git a/README.md b/README.md index 4876f76..468888d 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ However, some things might be broken :grin: - [Manual install](#manual-install) - [CLI install](#cli-install) - [Add resource reference](#add-resource-reference) +- [Data processing steps](#data-processing-steps) - [Using the card](#using-the-card) - [Main Options](#main-options) - [`series` Options](#series-options) @@ -36,6 +37,7 @@ However, some things might be broken :grin: - [`func` Options](#func-options) - [`chart_type` Options](#chart_type-options) - [`span` Options](#span-options) + - [`transform` Option](#transform-option) - [`data_generator` Option](#data_generator-option) - [Apex Charts Options Example](#apex-charts-options-example) - [Layouts](#layouts) @@ -92,6 +94,12 @@ Else, if you prefer the graphical editor, use the menu to add the resource: 3. Enter URL `/local/apexcharts-card.js` and select type "JavaScript Module". 4. Restart Home Assistant. +## Data processing steps + +This diagrams shows how your data goes through all the steps allowed by this card: + +![data_processing_steps](docs/data_processing_chart.png) + ## Using the card ### Main Options @@ -138,6 +146,7 @@ The card stricly validates all the options available (but not for the `apex_conf | `fill_raw` | string | `'null'` | NEXT_VERSION | If there is any missing value in the history, `last` will replace them with the last non-empty state, `zero` will fill missing values with `0`, `'null'` will fill missing values with `null`. This is applied before `group_by` options | | `group_by` | object | | v1.0.0 | See [group_by](#group_by-options) | | `invert` | boolean | `false` | v1.2.0 | Negates the data (`1` -> `-1`). Usefull to display opposites values like network in (standard)/out (inverted) | +| `transform` | string | | NEXT_VERSION | Transform your raw data in any way you like. See [transform](#transform-option) | | `data_generator` | string | | v1.2.0 | See [data_generator](#data_generator-option) | | `offset` | string | | v1.3.0 | 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`, ... | | `min` | number | `0` | v1.4.0 | Only used when `chart_type = radialBar`, see [chart_type](#chart_type-options). Used to convert the value into a percentage. Minimum value of the sensor | @@ -259,6 +268,42 @@ Eg: end: day ``` +### `transform` Option + +With transform, you can modify raw data comming from Home-Assistant's history using a javascript function. + +Some of the things you can do: +* Transform any state into a number (for eg. for binary_sensors) +* Apply a different scale to your data (eg: divide by 1024 to convert bits into Kbits) +* Do anything that javascript allows with the value + +Your javascript code will receive: +* `x`: a state or a value of the attribute if you defined one (it can be a `string`, `null` or a `number` depending on the entity type you've assigned) +* `hass`: the full `hass` object (`hass.states['other.entity']` to get the state object of another entity for eg.) + +And should return a `number`, a `float` or `null`. + +Some examples: +* Convert `binary_sensor` to numbers (`1` is `on`, `0` is `off`) + ```yaml + type: custom:apexcharts-card + update_delay: 3s + update_interval: 1min + series: + - entity: binary_sensor.heating + transform: "return x === 'on' ? 1 : 0;" + ``` + +* Scale a sensor: + ```yaml + type: custom:apexcharts-card + update_delay: 3s + update_interval: 1min + series: + - entity: sensor.bandwidth + transform: "return x / 1024;" + ``` + ### `data_generator` Option Before we start, to learn javascript, google is your friend or ask for help on the [forum](https://community.home-assistant.io/t/apexcharts-card-a-highly-customizable-graph-card/272877) :slightly_smiling_face: @@ -373,12 +418,12 @@ For code junkies, you'll find the default options I use in [`src/apex-layouts.ts Not ordered by priority: * [X] ~~Support more types of charts (pie, radial, polar area at least)~~ -* [ ] Support for `binary_sensors` +* [X] ~~Support for `binary_sensors`~~ * [X] ~~Support for aggregating data with exact boundaries (ex: aggregating data with `1h` could aggregate from `2:00:00am` to `2:59:59am` then `3:00:00am` to `3:59:59` exactly, etc...)~~ * [X] ~~Display the graph from start of day, week, month, ... with support for "up to now" or until the "end of the period"~~ * [ ] Support for any number of Y-axis * [ ] Support for logarithmic -* [ ] Support for state mapping for non-numerical state sensors +* [X] ~~Support for state mapping for non-numerical state sensors~~ * [ ] Support for simple color threshold (easier to understand/write than the ones provided natively by ApexCharts) * [ ] Support for graph configuration templates à la [`button-card`](https://github.com/custom-cards/button-card/blob/master/README.md#configuration-templates) diff --git a/docs/data_processing_chart.drawio b/docs/data_processing_chart.drawio new file mode 100644 index 0000000..1e59fe4 --- /dev/null +++ b/docs/data_processing_chart.drawio @@ -0,0 +1 @@ +3VpZm6I4FP01PpYfu/iotffUTHctMzXVL34RwjKNhA6x1P71EyQRCCi4oLYvVclNDOSec7eEjno9md9jEHl/IhsGHUWy5x31pqMopqLRv4lgkQo0VU8FLvbtVCRnglf/F2RCiUmnvg3jwkSCUED8qCi0UBhCixRkAGM0K05zUFB8agRcWBK8WiAoS999m3h8W71M/gB91+NPlo1+OjIBfDLbSewBG81yIvW2o15jhEjamsyvYZDojuvl/XHxHjz9MO6/PMc/wd/DP97++ucqXexum5+stoBhSHZeOvrytHiJlGfp7WP0+DB+CKzIYT+RPkEwZfp6QBN4NYhjPyaAPk2RHmgL4QXTAVlwxWI0DW2YLC531OHM8wl8jYCVjM4ok6jMI5OADTt+EFyjAOHlb1XHcRTLovKYYPQD5kZsY2zoBh1puGemm0+ICZznEGc6uId0OyR5e4mNaoy+i2J3lnHDYErxcrTQmQwwNrqrdTON0wZT+hYAyCUAOooR0KcOIwxp002azjS0ksewkTHmA/YUA+KjkI/QSbmfHRQyW4emrVVBZipj1WgTMpX7EoaZzC2yBjS1LdCUJqBxkYOWZpTBYPycIj5wFS8d5oBO6EfzbIwv4lLIotGYbvwuh3G6ZPExddB7aDKextvDDqDpVFqqYZlw7LQIuyLCLvW6Da1Vbwl4tQT8gsY2UdcwtAdJ5KI9KwDUlVpFFcO5T/7NtT9oW6JbS3s3yfYl3lmwznZKjtEUW7CewQRgF5J69wTtQpBdC9mVVIGQvsGfYhhQB/ZZjNVVsLFHfkP+0pq4Z9CLFCmZfKoI9qt8cBQW0iSBa6awUKqpDQvxichxYliYs2TaSle7k0+r8DpbMu/ALJLKLBo9u++haX6N362p/Bg+f4S/vvM3r2XRcUijCJzRduSMuSYq1VDmUHToNQlCq/hRSh0SP3/haUP/zNKG/tkZMC/I6sJA78wMWABWE+J9UwsWXUFpoZZNWK7K/k/LiF7TxOC8KKEJiYCsal1T3ZEVmhAg5GaOncIEFrlpUTIh3vDOIvv60lbvJcynjfQNDkvRqlrntBRtnLueF0UPlqvuys+DUaJcBYWoTAqq9ScwhkGRCyDw3TAhCuUCpJnAMInnvgWCARuY+LadrDHEkNbGYLxcL6mAmDnRxfVhR7+p5NVmCouZw+rQkT2lkz/XW1Pe9DRJ348O3Hq7Qqxpr3CQ9RJgN34cBUtvde0BTEro7ZXyne7IQBVze/nEKZ/cKEuHc0J96IigEf132Um53NdrEeofFaEzzMobJ2H6eUU40frEirhphFN7QqbTay3CVZ5XlANcZXh7ZV2EiYdcFILgNpMOMwNNolc25wmhiPHmP0jIgl2ZgSlBVawSsG1Os1r2KNVc2ZMEslh1q82Ks0NhV+Vwfzvs8i5i04la7cmbsSfGTV3rppfcHPsIBmHsIDw5p7hXkSo2g279tWP9WVSvpbBXiU05JbzcK6zToX7aK6z1HqH2wplqcITB7LJtUlbrjbKtaqESG86WCw9eZsPgpZ8yeJlN7MQGBIxcGEIMCKVt+9bS+MOZFqxFuO8yysayMqC8tYhFwOGsZbvTc0TnLRUHMBFkOX3bIPZW2CSdb4AQiMOlRJEUvsJdcn3GrvBDO9c7oPHsdedqVoN5nEpQqAHE8q3xrfy6rOlIdWDFSefXl0qPfJSTzsYHmEW6bTCeTSedqqypRe0fhBqy3JXaOPms3mWDbyZiD0RJ0wngnPmFYe0XPKHNPyaVza6WSlg8NbqaZu7huXd0HacydVlTumaBJ7ue+oh8M45s7OWS6PVt8PJWIgy1QdKeqSfpWFpR5cJJC9FcNkvhu4olO0Rv2s0+ik7Ryb4sV2//Bw== \ No newline at end of file diff --git a/docs/data_processing_chart.png b/docs/data_processing_chart.png new file mode 100644 index 0000000000000000000000000000000000000000..fcf8324bb42e70b41b5a1a4b8d40c940e37a3335 GIT binary patch literal 31479 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 literal 0 HcmV?d00001 diff --git a/src/graphEntry.ts b/src/graphEntry.ts index 9df86fa..138afa9 100644 --- a/src/graphEntry.ts +++ b/src/graphEntry.ts @@ -11,8 +11,6 @@ import { ChartCardSpanExtConfig } from './types-config'; import * as pjson from '../package.json'; export default class GraphEntry { - private _history?: EntityEntryCache; - private _computedHistory?: EntityCachePoints; private _hass?: HomeAssistant; @@ -68,7 +66,6 @@ export default class GraphEntry { this._index = index; this._cache = cache; this._entityID = config.entity; - this._history = undefined; this._graphSpan = graphSpan; this._config = config; const now = new Date(); @@ -90,7 +87,7 @@ export default class GraphEntry { } get history(): EntityCachePoints { - return this._computedHistory || this._history?.data || []; + return this._computedHistory || []; } get index(): number { @@ -139,7 +136,7 @@ export default class GraphEntry { let history: EntityEntryCache | undefined = undefined; if (this._config.data_generator) { - this._history = this._generateData(start, end); + history = this._generateData(start, end); } else { this._realStart = new Date(start); this._realEnd = new Date(end); @@ -187,16 +184,20 @@ export default class GraphEntry { lastNonNull = history.data[history.data.length - 1][1]; } const newStateHistory: EntityCachePoints = newHistory[0].map((item) => { - let stateParsed: number | null = null; + let currentState: unknown = null; if (this._config.attribute) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion if (item.attributes && item.attributes![this._config.attribute]) { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - stateParsed = parseFloat(item.attributes![this._config.attribute]); + currentState = item.attributes![this._config.attribute]; } } else { - stateParsed = parseFloat(item.state); + currentState = item.state; + } + if (this._config.transform) { + currentState = this._applyTransform(currentState); } + let stateParsed: number | null = parseFloat(currentState as string); stateParsed = !Number.isNaN(stateParsed) ? stateParsed : null; if (stateParsed === null) { if (this._config.fill_raw === 'zero') { @@ -241,18 +242,24 @@ export default class GraphEntry { if (!history || history.data.length === 0) { this._updating = false; + this._computedHistory = undefined; return false; } - this._history = history; if (this._config.group_by.func !== 'raw') { - this._computedHistory = this._dataBucketer().map((bucket) => { + this._computedHistory = this._dataBucketer(history).map((bucket) => { return [bucket.timestamp, this._func(bucket.data)]; }); + } else { + this._computedHistory = history.data; } this._updating = false; return true; } + private _applyTransform(value: unknown): number | null { + return new Function('x', 'hass', `'use strict'; ${this._config.transform}`).call(this, value, this._hass); + } + private async _fetchRecent( start: Date | undefined, end: Date | undefined, @@ -300,7 +307,7 @@ export default class GraphEntry { }; } - private _dataBucketer(): HistoryBuckets { + private _dataBucketer(history: EntityEntryCache): HistoryBuckets { const ranges = Array.from(this._timeRange.reverseBy('milliseconds', { step: this._groupByDurationMs })).reverse(); // const res: EntityCachePoints[] = [[]]; let buckets: HistoryBuckets = []; @@ -308,7 +315,7 @@ export default class GraphEntry { buckets[index] = { timestamp: range.valueOf(), data: [] }; }); let lastNotNullValue: number | null = null; - this._history?.data.forEach((entry) => { + history?.data.forEach((entry) => { let properEntry = entry; // Fill null values if (properEntry[1] === null) { diff --git a/src/types-config-ti.ts b/src/types-config-ti.ts index fe49b3d..f81e198 100644 --- a/src/types-config-ti.ts +++ b/src/types-config-ti.ts @@ -57,6 +57,7 @@ export const ChartCardSeriesExternalConfig = t.iface([], { "func": t.opt("GroupByFunc"), "fill": t.opt("GroupByFill"), })), + "transform": t.opt("string"), }); 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')); diff --git a/src/types-config.ts b/src/types-config.ts index 66b62c6..b8c3f47 100644 --- a/src/types-config.ts +++ b/src/types-config.ts @@ -53,6 +53,7 @@ export interface ChartCardSeriesExternalConfig { func?: GroupByFunc; fill?: GroupByFill; }; + transform?: string; } export type ChartCardPrettyTime = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';