Skip to content

Commit

Permalink
feat(series): transform the data the way you want (#45)
Browse files Browse the repository at this point in the history
* New `transform` option in series

* Update Roadmap
  • Loading branch information
RomRider authored Feb 3, 2021
1 parent f653b70 commit 1cb6bb5
Show file tree
Hide file tree
Showing 8 changed files with 86 additions and 20 deletions.
16 changes: 10 additions & 6 deletions .devcontainer/configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -38,3 +38,7 @@ sensor:
name: random_0_1000
minimum: 0
maximum: 1000

input_boolean:
test_boolean:
name: Test Input Boolean
7 changes: 7 additions & 0 deletions .devcontainer/ui-lovelace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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;"
49 changes: 47 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions docs/data_processing_chart.drawio
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2021-02-03T18:36:46.645Z" agent="5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.104 Safari/537.36" etag="89JgA1b-oJGtWwMe9VCv" version="14.2.9" type="device"><diagram id="C5RBs43oDa-KdzZeNtuy" name="Page-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==</diagram></mxfile>
Binary file added docs/data_processing_chart.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 19 additions & 12 deletions src/graphEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -90,7 +87,7 @@ export default class GraphEntry {
}

get history(): EntityCachePoints {
return this._computedHistory || this._history?.data || [];
return this._computedHistory || [];
}

get index(): number {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -300,15 +307,15 @@ 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 = [];
ranges.forEach((range, index) => {
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) {
Expand Down
1 change: 1 addition & 0 deletions src/types-config-ti.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
1 change: 1 addition & 0 deletions src/types-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface ChartCardSeriesExternalConfig {
func?: GroupByFunc;
fill?: GroupByFill;
};
transform?: string;
}

export type ChartCardPrettyTime = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year';
Expand Down

0 comments on commit 1cb6bb5

Please sign in to comment.