From 17004a30b77733e5b09ebe7b4bbb4b7d3b265688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20W?= Date: Mon, 8 Feb 2021 13:16:46 +0100 Subject: [PATCH] feat(config_tpl): Create a config template and use it everywhere with `config_templates`, apply the same config to every series with `all_series_config` and add `color_list` to define your color list in one shot (#64) * Initial config templates support * Display version in error card * Update documentation --- .devcontainer/ui-lovelace.yaml | 23 ++++ README.md | 153 +++++++++++++++++++++-- src/apexcharts-card.ts | 222 ++++++++++++++++++--------------- src/types-config-ti.ts | 38 ++++++ src/types-config.ts | 42 +++++++ src/utils.ts | 43 ++++++- 6 files changed, 405 insertions(+), 116 deletions(-) diff --git a/.devcontainer/ui-lovelace.yaml b/.devcontainer/ui-lovelace.yaml index f943a9e..c1667bc 100644 --- a/.devcontainer/ui-lovelace.yaml +++ b/.devcontainer/ui-lovelace.yaml @@ -1,3 +1,19 @@ +apexcharts_card_templates: + test2: + color_list: + - red + - red + test: + config_templates: test2 + graph_span: 24h + color_list: + - black + all_series_config: + group_by: + duration: 1h + func: avg + stroke_width: 2 + views: - title: Main panel: true @@ -586,3 +602,10 @@ views: color: green - value: 25 color: orange + + - type: custom:apexcharts-card + config_templates: + - test + series: + - entity: sensor.temperature + - entity: sensor.temperature diff --git a/README.md b/README.md index 1305fbf..f89dcd5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![License](https://img.shields.io/github/license/custom-cards/button-card.svg)](LICENSE) +[![License](https://img.shields.io/github/license/RomRider/apexcharts-card.svg)](LICENSE) [![HACS Supported](https://img.shields.io/badge/HACS-Supported-green.svg)](https://github.com/custom-components/hacs) ![Downloads](https://img.shields.io/github/downloads/RomRider/apexcharts-card/total) ![GitHub Activity](https://img.shields.io/github/commit-activity/y/RomRider/apexcharts-card.svg?label=commits) @@ -42,6 +42,9 @@ However, some things might be broken :grin: - [`data_generator` Option](#data_generator-option) - [Apex Charts Options Example](#apex-charts-options-example) - [Layouts](#layouts) + - [Configuration Templates](#configuration-templates) + - [General](#general) + - [`all_series_config` options](#all_series_config-options) - [Experimental features](#experimental-features) - [Configuration options](#configuration-options) - [`color_threshold` experimental feature](#color_threshold-experimental-feature) @@ -122,6 +125,9 @@ The card stricly validates all the options available (but not for the `apex_conf | ---- | :--: | :-----: | :---: | ----------- | | :white_check_mark: `type` | string | | v1.0.0 | `custom:apexcharts-card` | | :white_check_mark: `series` | array | | v1.0.0 | See [series](#series-options) | +| `config_templates` | array | | NEXT_VERSION | Define a configuration once and reuse it multiple times. See [config_templates](#configuration-templates) | +| `color_list` | array | | NEXT_VERSION | Define the array of colors applied to the series. Will be overriden by each serie's color if defined. Usefull for `config_templates` mainly. | +| `all_series_config` | object | | NEXT_VERSION | If something is defined here it will apply this config to all the series. It accepts the same options as a serie minus `entity`. It is useful to avoid repetition but the same thing can be achieve in each serie individually. See [series](#series-options) and [all_series_config](#all_series_config-options) for an example | | `chart_type` | string | `line` | v1.4.0 | See [chart_type](#chart_type-options) | | `update_interval` | string | | v1.1.0 | By default the card updates on every state change. Setting this overrides the behaviour. Valid values are any time string, eg: `1h`, `12min`, `1d`, `1h25`, `10sec`, ... | | `update_delay` | string | `1500ms` | v1.4.0 | If the chart doesn't display the last state but the one before, you'll want to increase this value, don't go over `10s`, it's not necessary. You'll also want to increase this value if you are using `attribute` in the `series`. Valid values are any time strings. This is because of how Home-Assistant works with history, see [here](https://www.home-assistant.io/integrations/recorder/#commit_interval) | @@ -411,7 +417,8 @@ This is how you could change some options from ApexCharts as described on the [` Hundreds of options are available and it is not possible to describe them all here so check over there and ask on the [forum](https://community.home-assistant.io/t/apexcharts-card-a-highly-customizable-graph-card/272877) if you need help with using them. -Some options might not work in the context of this card. +* :warning: Some options might not work in the context of this card. +* :warning: Everything which is available through the default config of this card shouldn't be defined in `apex_config`. If you do, it might break. ```yaml type: custom:apexcharts-card @@ -434,6 +441,132 @@ For now, only `minimal` is supported: It will remove the grid, the axis and disp For code junkies, you'll find the default options I use in [`src/apex-layouts.ts`](src/apex-layouts.ts) +### Configuration Templates + +#### General + +- Define your config template in the main lovelace configuration and then use it in your cards. This will avoid a lot of repetitions! It's basically YAML anchors, but without using YAML anchors and is very useful if you split your config in multiple files 😄 +- You can overload any parameter with a new one +- Arrays will be merged by matching the index +- You can also inherit another template from within a template. +- You can inherit multiple templates at once by making it an array. In this case, the templates will be merged together with the current configuration in the order they are defined. This happens recursively. + + ```yaml + type: custom:apexcharts-card + config_templates: + - template1 + - template2 + # or + type: custom:apexcharts-card + config_templates: template1 + ``` + +The card templates will be applied in the order they are defined: `template2` will be merged with `template1` and then the local config will be merged with the result. You can still chain templates together (ie. define template in a apexcharts-card template. It will follow the path recursively). + +Make sure which type of lovelace dashboard you are using before changing the main lovelace configuration: + * **`managed`** changes are managed by lovelace UI - add the template configuration to configuration in raw editor + * go to your dashboard + * click three dots and `Edit dashboard` button + * click three dots again and click `Raw configuration editor` button + * **`yaml`** - add template configuration to your dashboard file (`ui-lovelace.yaml` for eg.) + +**Note:** Templates have to be defined in all dashboards, they are not shared. + +To give you an idea where to put those (in your dashboard file/RAW editor): +```yaml +apexcharts_card_templates: + default: + color_list: ['red', 'green', 'blue'] + + bandwidth_chart: + graph_span: 24h + config_templates: default + header: + show: true + show_states: true + colorize_states: true + all_series_config: + stroke_width: 2 + opacity: 0.3 + type: area + +views: + - title: Main + panel: true + cards: + [...] +``` + +And then where you define your card, you can consume those templates, and/or overload it: + +```yaml +- type: custom:apexcharts-card + template: bandwidth_chart + header: + title: WAN Bandwidth + series: + - entity: sensor.wan_download + - entity: sensor.wan_upload + invert: true +``` + +In the end, this would produce the same result as but it's shorter and you can reuse that template elsewhere: +```yaml +- type: custom:apexcharts-card + graph_span: 24h + header: + title: WAN Bandwidth + show: true + show_states: true + colorize_states: true + all_series_config: + stroke_width: 2 + opacity: 0.3 + type: area + color_list: ['red', 'green', 'blue'] + series: + - entity: sensor.wan_download + - entity: sensor.wan_upload + invert: true +``` + +#### `all_series_config` options + +This will allow you to apply some settings to all the series avoiding repetition. It's just syntaxic sugar and doesn't add more features. + +Eg: +```yaml +- type: custom:apexcharts-card + graph_span: 24h + all_series_config: + stroke_width: 2 + type: area + transform: return x / 1024; + unit: Mb/s + series: + - entity: sensor.wan_download + - entity: sensor.wan_upload + invert: true +``` + +Generates the same result as repeating the configuration in each series: +```yaml +- type: custom:apexcharts-card + graph_span: 24h + series: + - entity: sensor.wan_download + stroke_width: 2 + type: area + transform: return x / 1024; + unit: Mb/s + - entity: sensor.wan_upload + invert: true + stroke_width: 2 + type: area + transform: return x / 1024; + unit: Mb/s +``` + ## Experimental features :warning: You enter the danger zone :warning: @@ -529,7 +662,7 @@ Not ordered by priority: * [ ] Support for logarithmic * [X] ~~Support for state mapping for non-numerical state sensors~~ * [X] ~~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) +* [X] ~~Support for graph configuration templates à la [`button-card`](https://github.com/custom-cards/button-card/blob/master/README.md#configuration-templates)~~ ## Examples @@ -636,10 +769,8 @@ series: ```yaml type: custom:apexcharts-card graph_span: 1d - apex_config: - stroke: - # Will affect all the series - width: 2 + all_series_config: + stroke_width: 2 series: - entity: sensor.temperature - entity: sensor.humidity @@ -650,15 +781,11 @@ series: ```yaml type: custom:apexcharts-card graph_span: 1d - apex_config: - stroke: - # 2 will affect sensor.temperature - # 4 will affect sensor.humidity - # You need as much values here as the number of series - width: [2, 4] series: - entity: sensor.temperature + stroke_width: 2 - entity: sensor.humidity + stroke_width: 6 ``` ### Use apexcharts-card with auto-entities diff --git a/src/apexcharts-card.ts b/src/apexcharts-card.ts index 274b048..2be52c2 100644 --- a/src/apexcharts-card.ts +++ b/src/apexcharts-card.ts @@ -15,7 +15,9 @@ import { getPercentFromValue, interpolateColor, log, + mergeConfigTemplates, mergeDeep, + mergeDeepConfig, offsetData, prettyPrintTime, validateInterval, @@ -107,7 +109,7 @@ class ChartsCard extends LitElement { private _colors: string[] = []; - private _headerColors: string[] = [...DEFAULT_COLORS]; + private _headerColors: string[] = []; private _graphSpan: number = HOUR_24; @@ -239,115 +241,131 @@ class ChartsCard extends LitElement { } public setConfig(config: ChartCardExternalConfig) { - const configDup = JSON.parse(JSON.stringify(config)); - if (configDup.entities) { - configDup.series = configDup.entities; - delete configDup.entities; - } - const { ChartCardExternalConfig } = createCheckers(exportedTypeSuite); - if (!configDup.experimental?.disable_config_validation) { - ChartCardExternalConfig.strictCheck(configDup); - } - if (configDup.update_interval) { - this._interval = validateInterval(configDup.update_interval, 'update_interval'); - } - if (configDup.graph_span) { - this._graphSpan = validateInterval(configDup.graph_span, 'graph_span'); - } - if (configDup.span?.offset) { - this._offset = validateOffset(configDup.span.offset, 'span.offset'); + let configDup: ChartCardExternalConfig = JSON.parse(JSON.stringify(config)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((configDup as any).entities) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + configDup.series = (configDup as any).entities; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (configDup as any).entities; } - if (configDup.span?.end && configDup.span?.start) { - throw new Error(`span: Only one of 'start' or 'end' is allowed.`); + configDup = configDup as ChartCardExternalConfig; + if (configDup.config_templates && configDup.config_templates.length > 0) { + configDup = mergeConfigTemplates(getLovelace(), configDup); } - configDup.series.forEach((serie, index) => { - if (serie.offset) { - this._seriesOffset[index] = validateOffset(serie.offset, `series[${index}].offset`); + try { + const { ChartCardExternalConfig } = createCheckers(exportedTypeSuite); + if (!configDup.experimental?.disable_config_validation) { + ChartCardExternalConfig.strictCheck(configDup); } - }); - if (configDup.update_delay) { - this._updateDelay = validateInterval(configDup.update_delay, `update_delay`); - } - - this._config = mergeDeep( - { - graph_span: DEFAULT_GRAPH_SPAN, - cache: true, - useCompress: false, - show: { loading: true }, - }, - configDup, - ); - - if (this._config) { - // this._colors = [...DEFAULT_COLORS]; - this._graphs = this._config.series.map((serie, index) => { - if (!this._headerColors[index]) { - this._headerColors[index] = this._headerColors[index % DEFAULT_COLORS.length]; - } - if (serie.color) { - this._headerColors[index] = serie.color; - } - serie.fill_raw = serie.fill_raw || DEFAULT_FILL_RAW; - serie.extend_to_end = serie.extend_to_end !== undefined ? serie.extend_to_end : true; - serie.type = this._config?.chart_type ? undefined : serie.type || DEFAULT_SERIE_TYPE; - serie.unit = this._config?.chart_type === 'radialBar' ? '%' : serie.unit; - if (!serie.group_by) { - serie.group_by = { duration: DEFAULT_DURATION, func: DEFAULT_FUNC, fill: DEFAULT_GROUP_BY_FILL }; - } else { - serie.group_by.duration = serie.group_by.duration || DEFAULT_DURATION; - serie.group_by.func = serie.group_by.func || DEFAULT_FUNC; - serie.group_by.fill = serie.group_by.fill || DEFAULT_GROUP_BY_FILL; - } - if (!serie.show) { - serie.show = { - legend_value: DEFAULT_SHOW_LEGEND_VALUE, - in_header: DEFAULT_SHOW_IN_HEADER, - in_chart: DEFAULT_SHOW_IN_CHART, - }; - } else { - serie.show.legend_value = - serie.show.legend_value === undefined ? DEFAULT_SHOW_LEGEND_VALUE : serie.show.legend_value; - serie.show.in_chart = serie.show.in_chart === undefined ? DEFAULT_SHOW_IN_CHART : serie.show.in_chart; - serie.show.in_header = serie.show.in_header === undefined ? DEFAULT_SHOW_IN_HEADER : serie.show.in_header; - } - validateInterval(serie.group_by.duration, `series[${index}].group_by.duration`); - if (serie.color_threshold && serie.color_threshold.length > 0) { - const sorted: ChartCardColorThreshold[] = JSON.parse(JSON.stringify(serie.color_threshold)); - sorted.sort((a, b) => (a.value < b.value ? -1 : 1)); - serie.color_threshold = sorted; + if (configDup.all_series_config) { + configDup.series.forEach((serie, index) => { + const allDup = JSON.parse(JSON.stringify(configDup.all_series_config)); + configDup.series[index] = mergeDeepConfig(allDup, serie); + }); + } + if (configDup.update_interval) { + this._interval = validateInterval(configDup.update_interval, 'update_interval'); + } + if (configDup.graph_span) { + this._graphSpan = validateInterval(configDup.graph_span, 'graph_span'); + } + if (configDup.span?.offset) { + this._offset = validateOffset(configDup.span.offset, 'span.offset'); + } + if (configDup.span?.end && configDup.span?.start) { + throw new Error(`span: Only one of 'start' or 'end' is allowed.`); + } + configDup.series.forEach((serie, index) => { + if (serie.offset) { + this._seriesOffset[index] = validateOffset(serie.offset, `series[${index}].offset`); } + }); + if (configDup.update_delay) { + this._updateDelay = validateInterval(configDup.update_delay, `update_delay`); + } - if (serie.entity) { - const editMode = getLovelace()?.editMode; - // disable caching for editor - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const caching = editMode === true ? false : this._config!.cache; - const graphEntry = new GraphEntry( - index, + this._config = mergeDeep( + { + graph_span: DEFAULT_GRAPH_SPAN, + cache: true, + useCompress: false, + show: { loading: true }, + }, + configDup, + ); + + const defColors = this._config?.color_list || DEFAULT_COLORS; + if (this._config) { + this._graphs = this._config.series.map((serie, index) => { + if (!this._headerColors[index]) { + this._headerColors[index] = defColors[index % defColors.length]; + } + if (serie.color) { + this._headerColors[index] = serie.color; + } + serie.fill_raw = serie.fill_raw || DEFAULT_FILL_RAW; + serie.extend_to_end = serie.extend_to_end !== undefined ? serie.extend_to_end : true; + serie.type = this._config?.chart_type ? undefined : serie.type || DEFAULT_SERIE_TYPE; + serie.unit = this._config?.chart_type === 'radialBar' ? '%' : serie.unit; + if (!serie.group_by) { + serie.group_by = { duration: DEFAULT_DURATION, func: DEFAULT_FUNC, fill: DEFAULT_GROUP_BY_FILL }; + } else { + serie.group_by.duration = serie.group_by.duration || DEFAULT_DURATION; + serie.group_by.func = serie.group_by.func || DEFAULT_FUNC; + serie.group_by.fill = serie.group_by.fill || DEFAULT_GROUP_BY_FILL; + } + if (!serie.show) { + serie.show = { + legend_value: DEFAULT_SHOW_LEGEND_VALUE, + in_header: DEFAULT_SHOW_IN_HEADER, + in_chart: DEFAULT_SHOW_IN_CHART, + }; + } else { + serie.show.legend_value = + serie.show.legend_value === undefined ? DEFAULT_SHOW_LEGEND_VALUE : serie.show.legend_value; + serie.show.in_chart = serie.show.in_chart === undefined ? DEFAULT_SHOW_IN_CHART : serie.show.in_chart; + serie.show.in_header = serie.show.in_header === undefined ? DEFAULT_SHOW_IN_HEADER : serie.show.in_header; + } + validateInterval(serie.group_by.duration, `series[${index}].group_by.duration`); + if (serie.color_threshold && serie.color_threshold.length > 0) { + const sorted: ChartCardColorThreshold[] = JSON.parse(JSON.stringify(serie.color_threshold)); + sorted.sort((a, b) => (a.value < b.value ? -1 : 1)); + serie.color_threshold = sorted; + } + + if (serie.entity) { + const editMode = getLovelace()?.editMode; + // disable caching for editor // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._graphSpan!, + const caching = editMode === true ? false : this._config!.cache; + const graphEntry = new GraphEntry( + index, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this._graphSpan!, + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + caching, + serie, + this._config?.span, + ); + if (this._hass) graphEntry.hass = this._hass; + return graphEntry; + } + return undefined; + }); + this._config.series_in_graph = []; + this._config.series.forEach((serie, index) => { + if (serie.show.in_chart) { + this._colors.push(this._headerColors[index]); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - caching, - serie, - this._config?.span, - ); - if (this._hass) graphEntry.hass = this._hass; - return graphEntry; - } - return undefined; - }); - this._config.series_in_graph = []; - this._config.series.forEach((serie, index) => { - if (serie.show.in_chart) { - this._colors.push(this._headerColors[index]); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this._config!.series_in_graph.push(serie); - } - }); - this._headerColors = this._headerColors.slice(0, this._config?.series.length); + this._config!.series_in_graph.push(serie); + } + }); + this._headerColors = this._headerColors.slice(0, this._config?.series.length); + } + } catch (e) { + throw new Error(`/// apexcharts-card version ${pjson.version} /// ${e.message}`); } - // Full reset only happens in editor mode this._reset(); } diff --git a/src/types-config-ti.ts b/src/types-config-ti.ts index e7537ab..f62d5f7 100644 --- a/src/types-config-ti.ts +++ b/src/types-config-ti.ts @@ -6,6 +6,8 @@ import * as t from "ts-interface-checker"; export const ChartCardExternalConfig = t.iface([], { "type": t.lit('custom:apexcharts-card'), + "config_templates": t.opt(t.array("string")), + "color_list": t.opt(t.array("string")), "experimental": t.opt(t.iface([], { "color_threshold": t.opt("boolean"), "disable_config_validation": t.opt("boolean"), @@ -14,6 +16,7 @@ export const ChartCardExternalConfig = t.iface([], { "chart_type": t.opt(t.union(t.lit('line'), t.lit('scatter'), t.lit('pie'), t.lit('donut'), t.lit('radialBar'))), "update_interval": t.opt("string"), "update_delay": t.opt("string"), + "all_series_config": t.opt("ChartCardAllSeriesExternalConfig"), "series": t.array("ChartCardSeriesExternalConfig"), "graph_span": t.opt("string"), "hours_12": t.opt("boolean"), @@ -41,6 +44,40 @@ export const ChartCardSpanExtConfig = t.iface([], { "offset": t.opt("string"), }); +export const ChartCardAllSeriesExternalConfig = t.iface([], { + "attribute": t.opt("string"), + "name": t.opt("string"), + "type": t.opt(t.union(t.lit('line'), t.lit('column'), t.lit('area'))), + "color": t.opt("string"), + "opacity": t.opt("number"), + "curve": t.opt(t.union(t.lit('smooth'), t.lit('straight'), t.lit('stepline'))), + "stroke_width": t.opt("number"), + "extend_to_end": t.opt("boolean"), + "unit": t.opt("string"), + "invert": t.opt("boolean"), + "data_generator": t.opt("string"), + "float_precision": t.opt("number"), + "min": t.opt("number"), + "max": t.opt("number"), + "offset": t.opt("string"), + "fill_raw": t.opt("GroupByFill"), + "show": t.opt(t.iface([], { + "as_duration": t.opt("ChartCardPrettyTime"), + "legend_value": t.opt("boolean"), + "in_header": t.opt("boolean"), + "in_chart": t.opt("boolean"), + "datalabels": t.opt("boolean"), + "hidden_by_default": t.opt("boolean"), + })), + "group_by": t.opt(t.iface([], { + "duration": t.opt("string"), + "func": t.opt("GroupByFunc"), + "fill": t.opt("GroupByFill"), + })), + "transform": t.opt("string"), + "color_threshold": t.opt(t.array("ChartCardColorThreshold")), +}); + export const ChartCardSeriesExternalConfig = t.iface([], { "entity": "string", "attribute": t.opt("string"), @@ -99,6 +136,7 @@ export const ChartCardColorThreshold = t.iface([], { const exportedTypeSuite: t.ITypeSuite = { ChartCardExternalConfig, ChartCardSpanExtConfig, + ChartCardAllSeriesExternalConfig, ChartCardSeriesExternalConfig, ChartCardPrettyTime, GroupByFill, diff --git a/src/types-config.ts b/src/types-config.ts index e18b84a..1150a3c 100644 --- a/src/types-config.ts +++ b/src/types-config.ts @@ -1,5 +1,7 @@ export interface ChartCardExternalConfig { type: 'custom:apexcharts-card'; + config_templates?: string[]; + color_list?: string[]; experimental?: { color_threshold?: boolean; disable_config_validation?: boolean; @@ -8,6 +10,7 @@ export interface ChartCardExternalConfig { chart_type?: 'line' | 'scatter' | 'pie' | 'donut' | 'radialBar'; update_interval?: string; update_delay?: string; + all_series_config?: ChartCardAllSeriesExternalConfig; series: ChartCardSeriesExternalConfig[]; graph_span?: string; hours_12?: boolean; @@ -37,6 +40,45 @@ export interface ChartCardSpanExtConfig { end?: 'minute' | 'hour' | 'day' | 'week' | 'month' | 'year'; offset?: string; } + +export interface ChartCardAllSeriesExternalConfig { + attribute?: string; + name?: string; + type?: 'line' | 'column' | 'area'; + color?: string; + opacity?: number; + curve?: 'smooth' | 'straight' | 'stepline'; + stroke_width?: number; + extend_to_end?: boolean; + unit?: string; + invert?: boolean; + data_generator?: string; + float_precision?: number; + min?: number; + max?: number; + offset?: string; + fill_raw?: GroupByFill; + show?: { + as_duration?: ChartCardPrettyTime; + legend_value?: boolean; + in_header?: boolean; + in_chart?: boolean; + datalabels?: boolean; + hidden_by_default?: boolean; + }; + group_by?: { + duration?: string; + func?: GroupByFunc; + fill?: GroupByFill; + }; + transform?: string; + color_threshold?: ChartCardColorThreshold[]; +} + +// Need to duplicate because of https://github.com/gristlabs/ts-interface-checker/issues/35 +// export interface ChartCardSeriesExternalConfig extends ChartCardAllSeriesExternalConfig { +// entity: string; +// } export interface ChartCardSeriesExternalConfig { entity: string; attribute?: string; diff --git a/src/utils.ts b/src/utils.ts index 6228156..115fcca 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,7 +3,7 @@ import { compress as lzStringCompress, decompress as lzStringDecompress } from ' import { EntityCachePoints } from './types'; import { TinyColor } from '@ctrl/tinycolor'; import parse from 'parse-duration'; -import { ChartCardPrettyTime, ChartCardSeriesExternalConfig } from './types-config'; +import { ChartCardExternalConfig, ChartCardPrettyTime, ChartCardSeriesExternalConfig } from './types-config'; import { DEFAULT_MAX, DEFAULT_MIN, moment, NO_VALUE } from './const'; import { LovelaceConfig } from 'custom-card-helpers'; @@ -190,3 +190,44 @@ export function interpolateColor(a: string, b: string, factor: number): string { return `#${(((1 << 24) + (rr << 16) + (rg << 8) + rb) | 0).toString(16).slice(1)}`; } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export function mergeConfigTemplates(ll: any, config: ChartCardExternalConfig): ChartCardExternalConfig { + const tpl = config.config_templates; + if (!tpl) return config; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let result: any = {}; + const tpls = tpl && Array.isArray(tpl) ? tpl : [tpl]; + tpls?.forEach((template) => { + if (!ll.config.apexcharts_card_templates?.[template]) + throw new Error(`apexchart-card template '${template}' is missing from your config!`); + const res = mergeConfigTemplates(ll, JSON.parse(JSON.stringify(ll.config.apexcharts_card_templates[template]))); + result = mergeDeepConfig(result, res); + }); + result = mergeDeepConfig(result, config); + return result as ChartCardExternalConfig; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export function mergeDeepConfig(target: any, source: any): any { + const isObject = (obj) => obj && typeof obj === 'object'; + + if (!isObject(target) || !isObject(source)) { + return source; + } + + Object.keys(source).forEach((key) => { + const targetValue = target[key]; + const sourceValue = source[key]; + + if (Array.isArray(targetValue) && Array.isArray(sourceValue)) { + target[key] = mergeDeepConfig(targetValue, sourceValue); + } else if (isObject(targetValue) && isObject(sourceValue)) { + target[key] = mergeDeepConfig(Object.assign({}, targetValue), sourceValue); + } else { + target[key] = sourceValue; + } + }); + + return target; +}