Skip to content

Commit a1afd20

Browse files
committed
Better errors and renamed/deprecated offset to time_offset
1 parent 0662954 commit a1afd20

File tree

3 files changed

+74
-55
lines changed

3 files changed

+74
-55
lines changed

src/parse-config/defaults.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const defaultEntity = {
1515
},
1616
},
1717
internal: false,
18-
offset: "0s",
18+
time_offset: "0s",
1919
// extend_to_present: true unless using statistics. Defined inside parse-config.ts to avoid forward depndency
2020
unit_of_measurement: ({ meta }) => meta.unit_of_measurement || "",
2121
name: ({ meta, getFromConfig }) => {
@@ -44,7 +44,7 @@ export const defaultYaml = {
4444
hours_to_show: 1,
4545
refresh_interval: "auto",
4646
color_scheme: "category10",
47-
offset: "0s",
47+
time_offset: "0s",
4848
no_theme: false,
4949
no_default_layout: false,
5050
disable_pinch_to_zoom: false,

src/parse-config/parse-config.ts

Lines changed: 55 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ import filters from "../filters/filters";
1111
import bounds from "binary-search-bounds";
1212
import { has } from "lodash";
1313
import { StatisticValue } from "../recorder-types";
14-
import { HassEntity, YValue } from "../types";
14+
import { Config, HassEntity, YValue } from "../types";
1515

1616
class ConfigParser {
1717
private partiallyParsedConfig?: any;
18+
private errors?: Error[];
1819
private inputConfig?: any;
1920
private hass?: HomeAssistant;
2021
cache = new Cache();
@@ -25,8 +26,11 @@ class ConfigParser {
2526
raw_config: any;
2627
hass: HomeAssistant;
2728
cssVars: HATheme;
28-
}) {
29+
}): Promise<{ errors: Error[]; parsed: Config }> {
2930
if (this.busy) throw new Error("ParseConfig was updated while busy");
31+
this.partiallyParsedConfig = {};
32+
this.errors = [];
33+
3034
this.busy = true;
3135
try {
3236
this.hass = input.hass;
@@ -69,15 +73,19 @@ class ConfigParser {
6973
hass: input.hass,
7074
getFromConfig: () => "",
7175
};
72-
this.partiallyParsedConfig = {};
7376
this.inputConfig = config;
7477
for (const [key, value] of Object.entries(config)) {
75-
await this.evalNode({
76-
parent: this.partiallyParsedConfig,
77-
path: key,
78-
key: key,
79-
value,
80-
});
78+
try {
79+
await this.evalNode({
80+
parent: this.partiallyParsedConfig,
81+
path: key,
82+
key: key,
83+
value,
84+
});
85+
} catch (e) {
86+
console.warn(`Plotly Graph Card: Error parsing [${key}]`, e);
87+
this.errors?.push(e as Error);
88+
}
8189
}
8290

8391
// 3rd pass: decorate
@@ -112,7 +120,7 @@ class ConfigParser {
112120
this.partiallyParsedConfig.layout
113121
);
114122

115-
return this.partiallyParsedConfig;
123+
return { errors: this.errors, parsed: this.partiallyParsedConfig };
116124
} finally {
117125
this.busy = false;
118126
}
@@ -128,7 +136,6 @@ class ConfigParser {
128136
key: string;
129137
value: any;
130138
}) {
131-
errorIfDeprecated(path);
132139
if (path.match(/^defaults$/)) return;
133140
this.fnParam.path = path;
134141
this.fnParam.getFromConfig = (pathQuery: string) =>
@@ -137,7 +144,7 @@ class ConfigParser {
137144
if (
138145
path.match(/^entities\.\d+\./) && //isInsideEntity
139146
!path.match(
140-
/^entities\.\d+\.(entity|attribute|offset|statistic|period)/
147+
/^entities\.\d+\.(entity|attribute|time_offset|statistic|period)/
141148
) && //isInsideFetchParamNode
142149
!this.fnParam.xs && // alreadyFetchedData
143150
(is$fn(value) || path.match(/^entities\.\d+\.filters\.\d+$/)) // if function of filter
@@ -147,6 +154,8 @@ class ConfigParser {
147154
if (typeof value === "string" && value.startsWith("$fn")) {
148155
value = myEval(value.slice(3));
149156
}
157+
const error = getDeprecationError(path, value);
158+
if (error) this.errors?.push(error);
150159

151160
if (typeof value === "function") {
152161
/**
@@ -160,12 +169,18 @@ class ConfigParser {
160169
const me = Array.isArray(value) ? [] : {};
161170
parent[key] = me;
162171
for (const [childKey, childValue] of Object.entries(value)) {
163-
await this.evalNode({
164-
parent: me,
165-
path: `${path}.${childKey}`,
166-
key: childKey,
167-
value: childValue,
168-
});
172+
const childPath = `${path}.${childKey}`;
173+
try {
174+
await this.evalNode({
175+
parent: me,
176+
path: childPath,
177+
key: childKey,
178+
value: childValue,
179+
});
180+
} catch (e: any) {
181+
console.warn(`Plotly Graph Card: Error parsing [${childPath}]`, e);
182+
this.errors?.push(new Error(`at [${childPath}]: ${e?.message || e}`));
183+
}
169184
}
170185
} else {
171186
parent[key] = value;
@@ -238,7 +253,7 @@ class ConfigParser {
238253
if (!visible_range) {
239254
const hours_to_show = this.fnParam.getFromConfig("hours_to_show");
240255
const global_offset = parseTimeDuration(
241-
this.fnParam.getFromConfig("offset")
256+
this.fnParam.getFromConfig("time_offset")
242257
);
243258
const ms = hours_to_show * 60 * 60 * 1000;
244259
visible_range = [
@@ -262,7 +277,7 @@ class ConfigParser {
262277
...(statisticsParams ? statisticsParams : attribute ? { attribute } : {}),
263278
};
264279
const offset = parseTimeDuration(
265-
this.fnParam.getFromConfig(path + ".offset")
280+
this.fnParam.getFromConfig(path + ".time_offset")
266281
);
267282

268283
const range_to_fetch = [
@@ -402,17 +417,28 @@ type FnParam = {
402417
meta?: HassEntity["attributes"];
403418
};
404419

405-
function errorIfDeprecated(path: string) {
406-
if (path.match(/^entity\.\d+\.lambda$/))
407-
throw new Error("Lambdas were removed, use filters instead");
420+
function getDeprecationError(path: string, value: any) {
421+
const e = _getDeprecationError(path, value);
422+
if (e) return new Error(`at [${path}]: ${e}`);
423+
return null;
424+
}
425+
function _getDeprecationError(path: string, value: any) {
426+
if (path.match(/^offset$/)) return "renamed to time_offset in v3.0.0";
427+
if (path.match(/^entities\.\d+\.offset$/)) {
428+
try {
429+
parseTimeDuration(value);
430+
return 'renamed to time_offset in v3.0.0 to avoid conflicts with <a href="https://plotly.com/javascript/reference/bar/#bar-offset">bar-offsets</a>';
431+
} catch (e) {
432+
// bar-offsets are numbers without time unit
433+
}
434+
}
435+
if (path.match(/^entities\.\d+\.lambda$/))
436+
return "removed in v3.0.0, use filters instead";
408437
if (path.match(/^significant_changes_only$/))
409-
throw new Error(
410-
"significant_changes_only was removed, it is now always set to false"
411-
);
438+
return "removed in v3.0.0, it is now always set to false";
412439
if (path.match(/^minimal_response$/))
413-
throw new Error(
414-
"minimal_response was removed, if you need attributes use the 'attribute' parameter instead."
415-
);
440+
return "removed in v3.0.0, if you need attributes use the 'attribute' parameter instead.";
441+
return null;
416442
}
417443
export const getEntityIndex = (path: string) =>
418444
+path.match(/entities\.(\d+)/)![1];

src/plotly-graph-card.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class PlotlyGraph extends HTMLElement {
3232
data: (Plotly.PlotData & { entity: string })[];
3333
layout: Plotly.Layout;
3434
};
35-
msgEl: HTMLElement;
35+
errorMsgEl: HTMLElement;
3636
cardEl: HTMLElement;
3737
resetButtonEl: HTMLButtonElement;
3838
titleEl: HTMLElement;
@@ -94,21 +94,23 @@ export class PlotlyGraph extends HTMLElement {
9494
border: 0px;
9595
border-radius: 3px;
9696
}
97-
#msg {
97+
#error-msg {
9898
position: absolute;
99-
color: red;
99+
color: #ffffff;
100100
top: 0;
101-
background: rgba(0, 0, 0, 0.4);
101+
padding: 5px;
102+
background: rgba(203,0,0,0.8);
102103
overflow-wrap: break-word;
103104
width: 100%;
105+
display: none;
104106
}
105107
</style>
106108
<div id="title"> </div>
107109
<div id="plotly"> </div>
108-
<span id="msg"> </span>
110+
<span id="error-msg"> </span>
109111
<button id="reset" class="hidden">↻</button>
110112
</ha-card>`;
111-
this.msgEl = shadow.querySelector("#msg")!;
113+
this.errorMsgEl = shadow.querySelector("#error-msg")!;
112114
this.cardEl = shadow.querySelector("ha-card")!;
113115
this.contentEl = shadow.querySelector("div#plotly")!;
114116
this.resetButtonEl = shadow.querySelector("button#reset")!;
@@ -271,22 +273,8 @@ export class PlotlyGraph extends HTMLElement {
271273
// The user supplied configuration. Throw an exception and Lovelace will
272274
// render an error card.
273275
async setConfig(config: InputConfig) {
274-
try {
275-
this.msgEl.innerText = "";
276-
this.config = config;
277-
await this.plot({ should_fetch: false });
278-
} catch (e: any) {
279-
console.error(e);
280-
clearTimeout(this.handles.refreshTimeout!);
281-
if (typeof e.message === "string") {
282-
this.msgEl.innerText = e.message;
283-
} else {
284-
this.msgEl.innerText = JSON.stringify(e.message || "").replace(
285-
/\\"/g,
286-
'"'
287-
);
288-
}
289-
}
276+
this.config = config;
277+
await this.plot({ should_fetch: false });
290278
}
291279
// async _setConfig(config: InputConfig) {
292280
// config = JSON.parse(JSON.stringify(config));
@@ -340,12 +328,17 @@ export class PlotlyGraph extends HTMLElement {
340328
this.isBrowsing ? { visible_range: this.getVisibleRange() } : {},
341329
this.config
342330
);
343-
this.parsed_config = await this.configParser.update({
331+
const { errors, parsed } = await this.configParser.update({
344332
raw_config,
345333
hass: this.hass,
346334
cssVars: this.getCSSVars(),
347335
});
348-
// console.log("fetched", this.parsed_config);
336+
this.errorMsgEl.style.display = errors.length ? "block" : "none";
337+
this.errorMsgEl.innerHTML = errors
338+
.map((e) => "<span>" + (e || "See devtools console") + "</span>")
339+
.join("\n");
340+
this.parsed_config = parsed;
341+
console.log("fetched", this.parsed_config);
349342

350343
const { entities, layout, config, refresh_interval } = this.parsed_config;
351344
clearTimeout(this.handles.refreshTimeout!);

0 commit comments

Comments
 (0)