Skip to content

Commit a3b670b

Browse files
authored
Universal functions (#200)
* prototype notebook * class * about to start integration * last jupyter notebook * first version that runs * allow keys required for entity fetching to appear after a $fn if they are literals. * fixed multiple isssues * don't fetch non visible traces * disable parsing of functions that return functions * moved offset handling completely out of the cache * Implemented observed_range clipping * improved touch handler * Perf * cleanup * Recover after config error. * fixed filter output storage * removed debug logs * moved fnParams to the class * Reordered functions in parse-config and simplified a bit * hot reload on dev mode * remove perf logs and put class at the top * fixed default entity values * pass path instead of entityIdx as fnParam * $fn documentation * Added the rest of plotly (from 1.1mb to 2.2mb) for advanced plots. * Better errors and renamed/deprecated offset to time_offset * removed no_ha_theme and no_default_layout in favour of ha_theme and raw_plotly_config * re-fetch and autoscale on relevant config changes * Added "do you mean" to filter name errors * renamings * modularization and ha_theme unaffected by raw plotly config * docs and better css_vars for ha_theme * improved uirevision logic * simplifying defaults (1/2) * modularisation and fixed types * Warn on unknown time unit on derivative and integral * Removed old gotcha about the plot not rendering on exceptions * fix ha_theme default * organised the order of functions in parse-config a bit * new screenshot examples in readme * fix readme glitch
1 parent b51caf7 commit a3b670b

22 files changed

+1427
-968
lines changed

package-lock.json

Lines changed: 239 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "",
55
"main": "index.js",
66
"scripts": {
7-
"start": "esbuild src/plotly-graph-card.ts --servedir=dist --outdir=dist --bundle --sourcemap=inline",
7+
"start": "(node ./script/hot-reload.mjs & esbuild src/plotly-graph-card.ts --servedir=dist --outdir=dist --bundle --sourcemap=inline)",
88
"build": "esbuild src/plotly-graph-card.ts --outdir=dist --bundle --minify",
99
"test": "jest",
1010
"test:watch": "jest --watchAll",
@@ -18,15 +18,20 @@
1818
"@types/lodash": "^4.14.191",
1919
"@types/node": "^18.11.17",
2020
"@types/plotly.js": "^2.12.11",
21+
"@types/ws": "^8.5.4",
22+
"chokidar": "^3.5.3",
2123
"esbuild": "^0.16.10",
22-
"ts-jest": "^29.0.3"
24+
"ts-jest": "^29.0.3",
25+
"ws": "^8.12.0"
2326
},
2427
"dependencies": {
28+
"binary-search-bounds": "^2.0.5",
2529
"custom-card-helpers": "^1.9.0",
2630
"date-fns": "^2.29.3",
2731
"deepmerge": "^4.2.2",
2832
"lodash": "^4.17.21",
2933
"plotly.js": "^2.14.0",
34+
"propose": "^0.0.5",
3035
"simple-statistics": "^7.8.0"
3136
}
3237
}

readme.md

Lines changed: 128 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,20 @@
44
# Plotly Graph Card
55

66
<img src="https://user-images.githubusercontent.com/777196/202489269-184d2f30-e834-4bea-8104-5aedb7d6f2d0.gif" width="300" align="left">
7-
<img src="https://user-images.githubusercontent.com/777196/202489368-234c7fbd-b33d-4fd0-8885-80f9e704a191.gif" width="300" align="right">
7+
<img src="https://user-images.githubusercontent.com/777196/215353175-97118ea7-778b-41b7-96f2-7e52c1c396d3.gif" width="300" align="right" >
88

99
<br clear="both"/>
1010
<br clear="both"/>
1111

1212
<img src="https://user-images.githubusercontent.com/777196/148675247-6e838783-a02a-453c-96b5-8ce86094ece2.gif" width="300" align="left" >
13-
<img src="https://user-images.githubusercontent.com/37914650/148646429-34f32f23-20b8-4171-87d3-ca69d8ab34b1.JPG" width="300" align="right">
13+
<img width="300" alt="image" src="https://user-images.githubusercontent.com/777196/215352580-b2122f49-d37a-452f-9b59-e205bcfb76a1.png" align="right" >
1414

1515
<br clear="both"/>
1616
<br clear="both"/>
1717

18-
<img src="https://user-images.githubusercontent.com/777196/198649220-14af3cf2-8948-4174-8138-b669dce5319e.png" width="300" align="left" >
18+
<img width="300" alt="image" src="https://user-images.githubusercontent.com/777196/215352591-4eeec752-6abf-40cf-8214-a38c03d64b43.png" align="left" >
1919

20-
<img src="https://user-images.githubusercontent.com/52346449/142386216-dfc22660-b053-495d-906f-0ebccbdf985f.png" width="300" align="right" >
20+
<img src="https://user-images.githubusercontent.com/777196/198649220-14af3cf2-8948-4174-8138-b669dce5319e.png" width="300" align="right" >
2121

2222
<br clear="both"/>
2323

@@ -400,8 +400,6 @@ type: custom:plotly-graph
400400
entities:
401401
- entity: sensor.temperature_in_celsius
402402
name: living temperature in Farenheit # Overrides the entity name
403-
lambda: |- # Transforms the data
404-
(ys) => ys.map(y => (y × 9/5) + 32)
405403
unit_of_measurement: °F # Overrides the unit
406404
show_value: true # shows the last value as text
407405
texttemplate: >- # custom format for show_value
@@ -483,7 +481,7 @@ entities:
483481
# either statistics or states will be available, depending on if "statistics" are fetched or not
484482
# attributes will be available inside states only if an attribute is picked in the trace
485483
return {
486-
ys: states.map(state => +state?.attributes?.current_temperature - state?.attributes?.target_temperature + hass.states.get("sensor.inside_temp")),
484+
ys: states.map(state => +state?.attributes?.current_temperature - state?.attributes?.target_temperature + hass.states["sensor.temperature"].state,
487485
meta: { unit_of_measurement: "delta" }
488486
};
489487
},
@@ -678,19 +676,122 @@ entities:
678676
internal: true
679677
period: 5minute
680678
filters:
681-
- map_y: parseFloat(y)
679+
- map_y: parseFloat(y)
682680
- store_var: temp1
683681
- entity: sensor.temperature2
684682
period: 5minute
685683
name: sum of temperatures
686684
filters:
687685
- map_y: parseFloat(y)
688686
- map_y: y + vars.temp1.ys[i]
687+
```
688+
689+
### Universal functions
690+
691+
Javascript functions allowed everywhere in the yaml. Evaluation is top to bottom and shallow to deep (depth first traversal).
692+
693+
The returned value will be used as value for the property where it is found. E.g:
694+
695+
```js
696+
name: $fn ({ hass }) => hass.states["sensor.garden_temperature"].state
697+
```
698+
699+
### Available parameters:
689700

690-
### `lambda:` transforms (deprecated)
701+
Remember you can add a `console.log(the_object_you_want_to_inspect)` and see its content in the devTools console.
691702

692-
Deprecated. Use filters instead.
693-
Your old lambdas should still work for now but this API will be removed in March 2023.
703+
#### Everywhere:
704+
705+
- `getFromConfig: (path) => value;` Pass a path (e.g `entities.0.name`) and get back its value
706+
- `hass: HomeAssistant object;` For example: `hass.states["sensor.garden_temperature"].state` to get its current state
707+
- `vars: Record<string, any>;` You can communicate between functions with this. E.g `vars.temperatures = ys`
708+
- `path: string;` The path of the current function
709+
- `css_vars: HATheme;` The colors set by the active Home Assistant theme (see #ha_theme)
710+
711+
#### Only iniside entities
712+
713+
- `xs: Date[];` Array of timestamps
714+
- `ys: YValue[];` Array of values of the sensor/attribute/statistic
715+
- `statistics: StatisticValue[];` Array of statistics objects
716+
- `states: HassEntity[];` Array of state objects
717+
- `meta: HassEntity["attributes"];` The current attributes of the sensor
718+
719+
#### Gotchas
720+
721+
- The following entity attributes are required for fetching, so if another function needs the entity data it needs to be declared below them. `entity`,`attribute`,`offset`,`statistic`,`period`
722+
- Functions are allowed for those properties (`entity`, `attribute`, ...) but they do not receive entity data as parameters. You can still use the `hass` parameter to get the last state of an entity if you need to.
723+
- Functions cannot return functions for performance reasons. (feature request if you need this)
724+
- Defaults are not applied to the subelements returned by a function. (feature request if you need this)
725+
- You can get other values from the yaml with the `getFromConfig` parameter, but if they are functions they need to be defined before.
726+
727+
#### Adding the last value to the entitiy's name
728+
729+
```yaml
730+
type: custom:plotly-graph
731+
entities:
732+
- entity: sensor.garden_temperature
733+
name: |
734+
$fn ({ ys,meta }) =>
735+
meta.friendly_name + " " + ys[ys.length - 1]
736+
```
737+
738+
#### Sharing data across functions
739+
740+
```yaml
741+
type: custom:plotly-graph
742+
entities:
743+
- entity: sensor.garden_temperature
744+
745+
# the fn attribute has no meaning, it is just a placeholder to put a function there. It can be any name not used by plotly
746+
fn: |
747+
$fn ({ ys, vars }) =>
748+
vars.title = ys[ys.length - 1]
749+
title: $fn ({ vars }) => vars.title
750+
```
751+
752+
#### Histograms
753+
754+
```yaml
755+
type: custom:plotly-graph
756+
entities:
757+
- entity: sensor.openweathermap_temperature
758+
x: $fn ({ys,vars}) => ys
759+
type: histogram
760+
title: Temperature Histogram last 10 days
761+
hours_to_show: 240
762+
raw_plotly_config: true
763+
layout:
764+
margin:
765+
t: 0
766+
l: 50
767+
b: 40
768+
height: 285
769+
xaxis:
770+
autorange: true
771+
```
772+
773+
#### custom hover text
774+
775+
```yaml
776+
type: custom:plotly-graph
777+
title: hovertemplate
778+
entities:
779+
- entity: climate.living
780+
attribute: current_temperature
781+
customdata: |
782+
$fn ({states}) =>
783+
states.map( ({state, attributes}) =>({
784+
...attributes,
785+
state
786+
})
787+
)
788+
hovertemplate: |-
789+
<br> <b>Mode:</b> %{customdata.state}<br>
790+
<b>Target:</b>%{y}</br>
791+
<b>Current:</b>%{customdata.current_temperature}
792+
<extra></extra>
793+
hours_to_show: 24
794+
```
694795

695796
## Default trace & axis styling
696797

@@ -715,45 +816,42 @@ defaults:
715816
To define layout aspects, like margins, title, axes names, ...
716817
Anything from https://plotly.com/javascript/reference/layout/.
717818

718-
### disable default layout:
819+
### Home Assistant theming:
820+
821+
Toggle Home Assistant theme colors:
719822

720-
Use this if you want to use plotly default layout instead. Very useful for heavy customization while following pure plotly examples.
823+
- card-background-color
824+
- primary-background-color
825+
- primary-color
826+
- primary-text-color
827+
- secondary-text-color
721828

722829
```yaml
723830
type: custom:plotly-graph
724831
entities:
725832
- entity: sensor.temperature_in_celsius
726-
no_default_layout: true
833+
ha_theme: false #defaults to true
727834
```
728835

729-
### disable Home Assistant themes:
836+
### Raw plotly config:
837+
838+
Toggle all in-built defaults for layout and entitites. Useful when using histograms, 3d plots, etc.
839+
When true, the `x` and `y` properties of the traces won't be automatically filled with entity data, you need to use $fn for that.
730840

731841
```yaml
732842
type: custom:plotly-graph
733843
entities:
734844
- entity: sensor.temperature_in_celsius
735-
no_theme: true
845+
x: $fn ({xs}) => xs
846+
y: $fn ({ys}) => ys
847+
raw_plotly_config: true # defaults to false
736848
```
737849

738850
## config:
739851

740852
To define general configurations like enabling scroll to zoom, disabling the modebar, etc.
741853
Anything from https://plotly.com/javascript/configuration-options/.
742854

743-
## significant_changes_only
744-
745-
When true, will tell HA to only fetch datapoints with a different state as the one before.
746-
More here: https://developers.home-assistant.io/docs/api/rest/ under `/api/history/period/<timestamp>`
747-
748-
Caveats:
749-
750-
1. zana-37 repoorts that `minimal_response: false` needs to be set to get all non-significant datapoints [here](https://github.com/dbuezas/lovelace-plotly-graph-card/issues/34#issuecomment-1085083597).
751-
2. This configuration will be ignored (will be true) while fetching [Attribute Values](#Attribute-Values).
752-
753-
```yaml
754-
significant_changes_only: true # defaults to false
755-
```
756-
757855
## disable_pinch_to_zoom
758856

759857
```yaml
@@ -762,19 +860,6 @@ disable_pinch_to_zoom: true # defaults to false
762860

763861
When true, the custom implementations of pinch-to-zoom and double-tap-drag-to-zooming will be disabled.
764862

765-
## minimal_response
766-
767-
When true, tell HA to only return last_changed and state for states other than the first and last state (much faster).
768-
More here: https://developers.home-assistant.io/docs/api/rest/ under `/api/history/period/<timestamp>`
769-
770-
Caveats:
771-
772-
1. This configuration will be ignored (will be false) while fetching [Attribute Values](#Attribute-Values).
773-
774-
```yaml
775-
minimal_response: false # defaults to true
776-
```
777-
778863
## hours_to_show:
779864

780865
How many hours are shown.

script/hot-reload.mjs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// @ts-check
2+
import WebSocket, { WebSocketServer } from "ws";
3+
import chokidar from "chokidar";
4+
5+
const watchOptn = {
6+
// awaitWriteFinish: {stabilityThreshold:100, pollInterval:50},
7+
ignoreInitial: true,
8+
};
9+
async function hotReload() {
10+
const wss = new WebSocketServer({ port: 8081 });
11+
wss.on("connection", () => console.log(wss.clients.size));
12+
wss.on("close", () => console.log(wss.clients.size));
13+
const sendToClients = (
14+
/** @type {{ action: string; payload?: any }} */ message
15+
) => {
16+
wss.clients.forEach(function each(
17+
/** @type {{ readyState: number; send: (arg0: string) => void; }} */ client
18+
) {
19+
if (client.readyState === WebSocket.OPEN) {
20+
console.log("sending");
21+
client.send(JSON.stringify(message));
22+
}
23+
});
24+
};
25+
chokidar.watch("src", watchOptn).on("all", async (...args) => {
26+
console.log(args);
27+
try {
28+
sendToClients({ action: "update-app" });
29+
} catch (e) {
30+
console.error(e);
31+
sendToClients({ action: "error", payload: e.message });
32+
}
33+
});
34+
}
35+
36+
hotReload();

0 commit comments

Comments
 (0)