mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
20240828.0 (#21822)
This commit is contained in:
commit
8349e47c17
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@ -89,7 +89,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.3.5
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
@ -113,7 +113,7 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@v4.3.5
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
with:
|
||||
name: supervisor-bundle-stats
|
||||
path: build/stats/*.json
|
||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4.3.5
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@v4.3.5
|
||||
uses: actions/upload-artifact@v4.3.6
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send bundle stats and build information to RelativeCI
|
||||
uses: relative-ci/agent-action@v2.1.11
|
||||
uses: relative-ci/agent-action@v2.1.12
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
File diff suppressed because one or more lines are too long
@ -6,4 +6,4 @@ enableGlobalCache: false
|
||||
|
||||
nodeLinker: node-modules
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.4.0.cjs
|
||||
yarnPath: .yarn/releases/yarn-4.4.1.cjs
|
||||
|
@ -1,35 +1,76 @@
|
||||
// Tasks to generate entry HTML
|
||||
|
||||
import { getUserAgentRegex } from "browserslist-useragent-regexp";
|
||||
import {
|
||||
applyVersionsToRegexes,
|
||||
compileRegex,
|
||||
getPreUserAgentRegexes,
|
||||
} from "browserslist-useragent-regexp";
|
||||
import fs from "fs-extra";
|
||||
import gulp from "gulp";
|
||||
import { minify } from "html-minifier-terser";
|
||||
import template from "lodash.template";
|
||||
import path from "path";
|
||||
import { dirname, extname, resolve } from "node:path";
|
||||
import { htmlMinifierOptions, terserOptions } from "../bundle.cjs";
|
||||
import env from "../env.cjs";
|
||||
import paths from "../paths.cjs";
|
||||
|
||||
// macOS companion app has no way to obtain the Safari version used by WKWebView,
|
||||
// and it is not in the default user agent string. So we add an additional regex
|
||||
// to serve modern based on a minimum macOS version. We take the minimum Safari
|
||||
// major version from browserslist and manually map that to a supported macOS
|
||||
// version. Note this assumes the user has kept Safari updated.
|
||||
const HA_MACOS_REGEX =
|
||||
/Home Assistant\/[\d.]+ \(.+; macOS (\d+)\.(\d+)(?:\.(\d+))?\)/;
|
||||
const SAFARI_TO_MACOS = {
|
||||
15: [10, 15, 0],
|
||||
16: [11, 0, 0],
|
||||
17: [12, 0, 0],
|
||||
18: [13, 0, 0],
|
||||
};
|
||||
|
||||
const getCommonTemplateVars = () => {
|
||||
const browserRegexes = getPreUserAgentRegexes({
|
||||
env: "modern",
|
||||
allowHigherVersions: true,
|
||||
mobileToDesktop: true,
|
||||
throwOnMissing: true,
|
||||
});
|
||||
const minSafariVersion = browserRegexes.find(
|
||||
(regex) => regex.family === "safari"
|
||||
)?.matchedVersions[0][0];
|
||||
const minMacOSVersion = SAFARI_TO_MACOS[minSafariVersion];
|
||||
if (!minMacOSVersion) {
|
||||
throw Error(
|
||||
`Could not find minimum MacOS version for Safari ${minSafariVersion}.`
|
||||
);
|
||||
}
|
||||
const haMacOSRegex = applyVersionsToRegexes(
|
||||
[
|
||||
{
|
||||
family: "ha_macos",
|
||||
regex: HA_MACOS_REGEX,
|
||||
matchedVersions: [minMacOSVersion],
|
||||
requestVersions: [minMacOSVersion],
|
||||
},
|
||||
],
|
||||
{ ignorePatch: true, allowHigherVersions: true }
|
||||
);
|
||||
return {
|
||||
useRollup: env.useRollup(),
|
||||
useWDS: env.useWDS(),
|
||||
modernRegex: compileRegex(browserRegexes.concat(haMacOSRegex)).toString(),
|
||||
};
|
||||
};
|
||||
|
||||
const renderTemplate = (templateFile, data = {}) => {
|
||||
const compiled = template(
|
||||
fs.readFileSync(templateFile, { encoding: "utf-8" })
|
||||
);
|
||||
return compiled({
|
||||
...data,
|
||||
useRollup: env.useRollup(),
|
||||
useWDS: env.useWDS(),
|
||||
modernRegex: getUserAgentRegex({
|
||||
env: "modern",
|
||||
allowHigherVersions: true,
|
||||
mobileToDesktop: true,
|
||||
throwOnMissing: true,
|
||||
}).toString(),
|
||||
// Resolve any child/nested templates relative to the parent and pass the same data
|
||||
renderTemplate: (childTemplate) =>
|
||||
renderTemplate(
|
||||
path.resolve(path.dirname(templateFile), childTemplate),
|
||||
data
|
||||
),
|
||||
renderTemplate(resolve(dirname(templateFile), childTemplate), data),
|
||||
});
|
||||
};
|
||||
|
||||
@ -63,10 +104,12 @@ const genPagesDevTask =
|
||||
publicRoot = ""
|
||||
) =>
|
||||
async () => {
|
||||
const commonVars = getCommonTemplateVars();
|
||||
for (const [page, entries] of Object.entries(pageEntries)) {
|
||||
const content = renderTemplate(
|
||||
path.resolve(inputRoot, inputSub, `${page}.template`),
|
||||
resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
...commonVars,
|
||||
latestEntryJS: entries.map((entry) =>
|
||||
useWDS
|
||||
? `http://localhost:8000/src/entrypoints/${entry}.ts`
|
||||
@ -81,7 +124,7 @@ const genPagesDevTask =
|
||||
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
|
||||
}
|
||||
);
|
||||
fs.outputFileSync(path.resolve(outputRoot, page), content);
|
||||
fs.outputFileSync(resolve(outputRoot, page), content);
|
||||
}
|
||||
};
|
||||
|
||||
@ -98,16 +141,18 @@ const genPagesProdTask =
|
||||
) =>
|
||||
async () => {
|
||||
const latestManifest = fs.readJsonSync(
|
||||
path.resolve(outputLatest, "manifest.json")
|
||||
resolve(outputLatest, "manifest.json")
|
||||
);
|
||||
const es5Manifest = outputES5
|
||||
? fs.readJsonSync(path.resolve(outputES5, "manifest.json"))
|
||||
? fs.readJsonSync(resolve(outputES5, "manifest.json"))
|
||||
: {};
|
||||
const commonVars = getCommonTemplateVars();
|
||||
const minifiedHTML = [];
|
||||
for (const [page, entries] of Object.entries(pageEntries)) {
|
||||
const content = renderTemplate(
|
||||
path.resolve(inputRoot, inputSub, `${page}.template`),
|
||||
resolve(inputRoot, inputSub, `${page}.template`),
|
||||
{
|
||||
...commonVars,
|
||||
latestEntryJS: entries.map((entry) => latestManifest[`${entry}.js`]),
|
||||
es5EntryJS: entries.map((entry) => es5Manifest[`${entry}.js`]),
|
||||
latestCustomPanelJS: latestManifest["custom-panel.js"],
|
||||
@ -115,8 +160,8 @@ const genPagesProdTask =
|
||||
}
|
||||
);
|
||||
minifiedHTML.push(
|
||||
minifyHtml(content, path.extname(page)).then((minified) =>
|
||||
fs.outputFileSync(path.resolve(outputRoot, page), minified)
|
||||
minifyHtml(content, extname(page)).then((minified) =>
|
||||
fs.outputFileSync(resolve(outputRoot, page), minified)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -532,15 +532,6 @@ export default {
|
||||
last_changed: "2018-07-19T10:44:46.200946+00:00",
|
||||
last_updated: "2018-07-19T10:44:46.200946+00:00",
|
||||
},
|
||||
"mailbox.demomailbox": {
|
||||
entity_id: "mailbox.demomailbox",
|
||||
state: "10",
|
||||
attributes: {
|
||||
friendly_name: "DemoMailbox",
|
||||
},
|
||||
last_changed: "2018-07-19T10:45:16.555210+00:00",
|
||||
last_updated: "2018-07-19T10:45:16.555210+00:00",
|
||||
},
|
||||
"input_select.living_room_preset": {
|
||||
entity_id: "input_select.living_room_preset",
|
||||
state: "Visitors",
|
||||
|
44
package.json
44
package.json
@ -25,15 +25,15 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.25.0",
|
||||
"@babel/runtime": "7.25.4",
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@codemirror/autocomplete": "6.18.0",
|
||||
"@codemirror/commands": "6.6.0",
|
||||
"@codemirror/language": "6.10.2",
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/legacy-modes": "6.4.1",
|
||||
"@codemirror/search": "6.5.6",
|
||||
"@codemirror/state": "6.4.1",
|
||||
"@codemirror/view": "6.30.0",
|
||||
"@codemirror/view": "6.33.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.12.5",
|
||||
"@formatjs/intl-displaynames": "6.6.8",
|
||||
@ -49,7 +49,7 @@
|
||||
"@fullcalendar/list": "6.1.15",
|
||||
"@fullcalendar/luxon3": "6.1.15",
|
||||
"@fullcalendar/timegrid": "6.1.15",
|
||||
"@lezer/highlight": "1.2.0",
|
||||
"@lezer/highlight": "1.2.1",
|
||||
"@lit-labs/context": "0.4.1",
|
||||
"@lit-labs/motion": "1.0.7",
|
||||
"@lit-labs/observers": "2.0.2",
|
||||
@ -80,7 +80,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "2.0.0",
|
||||
"@material/web": "2.1.0",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@polymer/paper-item": "3.0.1",
|
||||
@ -88,8 +88,8 @@
|
||||
"@polymer/paper-tabs": "3.1.0",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.4.5",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.5",
|
||||
"@vaadin/combo-box": "24.4.6",
|
||||
"@vaadin/vaadin-themable-mixin": "24.4.6",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@ -97,10 +97,10 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.9",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "4.4.3",
|
||||
"chart.js": "4.4.4",
|
||||
"color-name": "2.0.0",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.38.0",
|
||||
"core-js": "3.38.1",
|
||||
"cropperjs": "1.6.2",
|
||||
"date-fns": "3.6.0",
|
||||
"date-fns-tz": "3.1.3",
|
||||
@ -118,7 +118,7 @@
|
||||
"leaflet-draw": "1.0.4",
|
||||
"lit": "2.8.0",
|
||||
"luxon": "3.5.0",
|
||||
"marked": "13.0.3",
|
||||
"marked": "14.0.0",
|
||||
"memoize-one": "6.0.0",
|
||||
"node-vibrant": "3.2.1-alpha.1",
|
||||
"proxy-polyfill": "0.3.2",
|
||||
@ -130,7 +130,7 @@
|
||||
"sortablejs": "1.15.2",
|
||||
"stacktrace-js": "2.0.2",
|
||||
"superstruct": "2.0.2",
|
||||
"tinykeys": "2.1.0",
|
||||
"tinykeys": "3.0.0",
|
||||
"tsparticles-engine": "2.12.0",
|
||||
"tsparticles-preset-links": "2.12.0",
|
||||
"ua-parser-js": "1.0.38",
|
||||
@ -152,15 +152,15 @@
|
||||
"@babel/core": "7.25.2",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.2",
|
||||
"@babel/plugin-proposal-decorators": "7.24.7",
|
||||
"@babel/plugin-transform-runtime": "7.24.7",
|
||||
"@babel/preset-env": "7.25.3",
|
||||
"@babel/plugin-transform-runtime": "7.25.4",
|
||||
"@babel/preset-env": "7.25.4",
|
||||
"@babel/preset-typescript": "7.24.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.14.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.14.2",
|
||||
"@koa/cors": "5.0.0",
|
||||
"@lokalise/node-api": "12.7.0",
|
||||
"@octokit/auth-oauth-device": "7.1.1",
|
||||
"@octokit/plugin-retry": "7.1.1",
|
||||
"@octokit/rest": "21.0.1",
|
||||
"@octokit/rest": "21.0.2",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.4",
|
||||
"@rollup/plugin-commonjs": "26.0.1",
|
||||
@ -168,7 +168,7 @@
|
||||
"@rollup/plugin-node-resolve": "15.2.3",
|
||||
"@rollup/plugin-replace": "5.0.7",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.16",
|
||||
"@types/chromecast-caf-receiver": "6.0.17",
|
||||
"@types/chromecast-caf-sender": "1.0.10",
|
||||
"@types/color-name": "1.1.4",
|
||||
"@types/glob": "8.1.0",
|
||||
@ -202,8 +202,8 @@
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"eslint-plugin-lit": "1.14.0",
|
||||
"eslint-plugin-lit-a11y": "4.1.4",
|
||||
"eslint-plugin-unused-imports": "4.0.1",
|
||||
"eslint-plugin-wc": "2.1.0",
|
||||
"eslint-plugin-unused-imports": "4.1.3",
|
||||
"eslint-plugin-wc": "2.1.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"glob": "11.0.0",
|
||||
@ -213,10 +213,10 @@
|
||||
"gulp-rename": "2.0.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.4",
|
||||
"husky": "9.1.5",
|
||||
"instant-mocha": "1.5.2",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "15.2.8",
|
||||
"lint-staged": "15.2.9",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@ -239,7 +239,7 @@
|
||||
"transform-async-modules-webpack-plugin": "1.1.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.5.4",
|
||||
"webpack": "5.93.0",
|
||||
"webpack": "5.94.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.0.4",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
@ -258,5 +258,5 @@
|
||||
"sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
|
||||
"leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
|
||||
},
|
||||
"packageManager": "yarn@4.4.0"
|
||||
"packageManager": "yarn@4.4.1"
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20240809.0"
|
||||
version = "20240828.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
|
@ -40,7 +40,6 @@ import {
|
||||
mdiImageFilterFrames,
|
||||
mdiLightbulb,
|
||||
mdiLightningBolt,
|
||||
mdiMailbox,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMeterGas,
|
||||
mdiMicrophoneMessage,
|
||||
@ -119,7 +118,6 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
input_text: mdiFormTextbox,
|
||||
lawn_mower: mdiRobotMower,
|
||||
light: mdiLightbulb,
|
||||
mailbox: mdiMailbox,
|
||||
notify: mdiCommentAlert,
|
||||
number: mdiRayVertex,
|
||||
persistent_notification: mdiBell,
|
||||
|
@ -71,8 +71,7 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
if (
|
||||
attributes.device_class === "duration" &&
|
||||
attributes.unit_of_measurement &&
|
||||
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] &&
|
||||
entity?.display_precision === undefined
|
||||
UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement]
|
||||
) {
|
||||
try {
|
||||
return formatDuration(state, attributes.unit_of_measurement);
|
||||
|
@ -26,7 +26,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
humidifier: ["on", "off"],
|
||||
input_boolean: ["on", "off"],
|
||||
input_button: [],
|
||||
lawn_mower: ["error", "paused", "mowing", "docked"],
|
||||
lawn_mower: ["error", "paused", "mowing", "returning", "docked"],
|
||||
light: ["on", "off"],
|
||||
lock: [
|
||||
"jammed",
|
||||
|
6
src/components/chart/click_is_touch.ts
Normal file
6
src/components/chart/click_is_touch.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { ChartEvent } from "chart.js";
|
||||
|
||||
export const clickIsTouch = (event: ChartEvent): boolean =>
|
||||
!(event.native instanceof MouseEvent) ||
|
||||
(event.native instanceof PointerEvent &&
|
||||
event.native.pointerType !== "mouse");
|
@ -16,6 +16,7 @@ import {
|
||||
HaChartBase,
|
||||
MIN_TIME_BETWEEN_UPDATES,
|
||||
} from "./ha-chart-base";
|
||||
import { clickIsTouch } from "./click_is_touch";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
const parsed = parseFloat(value);
|
||||
@ -220,12 +221,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
onClick: (e: any) => {
|
||||
if (
|
||||
!this.clickForMoreInfo ||
|
||||
!(e.native instanceof MouseEvent) ||
|
||||
(e.native instanceof PointerEvent &&
|
||||
e.native.pointerType !== "mouse")
|
||||
) {
|
||||
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,7 @@ import {
|
||||
} from "./ha-chart-base";
|
||||
import type { TimeLineData } from "./timeline-chart/const";
|
||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
|
||||
import { clickIsTouch } from "./click_is_touch";
|
||||
|
||||
@customElement("state-history-chart-timeline")
|
||||
export class StateHistoryChartTimeline extends LitElement {
|
||||
@ -224,11 +225,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
onClick: (e: any) => {
|
||||
if (
|
||||
!this.clickForMoreInfo ||
|
||||
!(e.native instanceof MouseEvent) ||
|
||||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
|
||||
) {
|
||||
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -39,6 +39,7 @@ import type {
|
||||
ChartDatasetExtra,
|
||||
HaChartBase,
|
||||
} from "./ha-chart-base";
|
||||
import { clickIsTouch } from "./click_is_touch";
|
||||
|
||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
|
||||
mean: "mean",
|
||||
@ -278,11 +279,7 @@ export class StatisticsChart extends LitElement {
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
onClick: (e: any) => {
|
||||
if (
|
||||
!this.clickForMoreInfo ||
|
||||
!(e.native instanceof MouseEvent) ||
|
||||
(e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
|
||||
) {
|
||||
if (!this.clickForMoreInfo || clickIsTouch(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -45,15 +45,35 @@ export class HaConversationAgentPicker extends LitElement {
|
||||
if (!this._agents) {
|
||||
return nothing;
|
||||
}
|
||||
const value =
|
||||
this.value ??
|
||||
(this.required &&
|
||||
(!this.language ||
|
||||
this._agents
|
||||
.find((agent) => agent.id === "homeassistant")
|
||||
?.supported_languages.includes(this.language))
|
||||
? "homeassistant"
|
||||
: NONE);
|
||||
let value = this.value;
|
||||
if (!value && this.required) {
|
||||
// Select Home Assistant conversation agent if it supports the language
|
||||
for (const agent of this._agents) {
|
||||
if (
|
||||
agent.id === "conversation.home_assistant" &&
|
||||
agent.supported_languages.includes(this.language!)
|
||||
) {
|
||||
value = agent.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!value) {
|
||||
// Select the first agent that supports the language
|
||||
for (const agent of this._agents) {
|
||||
if (
|
||||
agent.supported_languages === "*" &&
|
||||
agent.supported_languages.includes(this.language!)
|
||||
) {
|
||||
value = agent.id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!value) {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
|
@ -68,8 +68,8 @@ export class HaExpansionPanel extends LitElement {
|
||||
></ha-svg-icon>
|
||||
`
|
||||
: ""}
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
<slot name="icons"></slot>
|
||||
</div>
|
||||
<div
|
||||
class="container ${classMap({ expanded: this.expanded })}"
|
||||
|
@ -21,13 +21,45 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ attribute: false }) public computeLabel?: (
|
||||
schema: HaFormSchema,
|
||||
data?: HaFormDataContainer
|
||||
data?: HaFormDataContainer,
|
||||
options?: { path?: string[] }
|
||||
) => string;
|
||||
|
||||
@property({ attribute: false }) public computeHelper?: (
|
||||
schema: HaFormSchema
|
||||
schema: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
) => string;
|
||||
|
||||
private _renderDescription() {
|
||||
const description = this.computeHelper?.(this.schema);
|
||||
return description ? html`<p>${description}</p>` : nothing;
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: HaFormSchema,
|
||||
data?: HaFormDataContainer,
|
||||
options?: { path?: string[] }
|
||||
) => {
|
||||
if (!this.computeLabel) return this.computeLabel;
|
||||
|
||||
return this.computeLabel(schema, data, {
|
||||
...options,
|
||||
path: [...(options?.path || []), this.schema.name],
|
||||
});
|
||||
};
|
||||
|
||||
private _computeHelper = (
|
||||
schema: HaFormSchema,
|
||||
options?: { path?: string[] }
|
||||
) => {
|
||||
if (!this.computeHelper) return this.computeHelper;
|
||||
|
||||
return this.computeHelper(schema, {
|
||||
...options,
|
||||
path: [...(options?.path || []), this.schema.name],
|
||||
});
|
||||
};
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
|
||||
@ -43,16 +75,17 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
<ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
|
||||
`
|
||||
: nothing}
|
||||
${this.schema.title}
|
||||
${this.schema.title || this.computeLabel?.(this.schema)}
|
||||
</div>
|
||||
<div class="content">
|
||||
${this._renderDescription()}
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.data}
|
||||
.schema=${this.schema.schema}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this.computeLabel}
|
||||
.computeHelper=${this.computeHelper}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
></ha-form>
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
@ -71,6 +104,9 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
|
||||
.content {
|
||||
padding: 12px;
|
||||
}
|
||||
.content p {
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
display: block;
|
||||
--expansion-panel-content-padding: 0;
|
||||
|
@ -31,7 +31,7 @@ const LOAD_ELEMENTS = {
|
||||
};
|
||||
|
||||
const getValue = (obj, item) =>
|
||||
obj ? (!item.name ? obj : obj[item.name]) : null;
|
||||
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
@ -73,10 +73,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
schema: any
|
||||
) => string | undefined;
|
||||
|
||||
@property({ attribute: false }) public localizeValue?: (
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
protected getFormProperties(): Record<string, any> {
|
||||
return {};
|
||||
}
|
||||
@ -149,7 +145,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
.disabled=${item.disabled || this.disabled || false}
|
||||
.placeholder=${item.required ? "" : item.default}
|
||||
.helper=${this._computeHelper(item)}
|
||||
.localizeValue=${this.localizeValue}
|
||||
.required=${item.required || false}
|
||||
.context=${this._generateContext(item)}
|
||||
></ha-selector>`
|
||||
@ -204,9 +199,10 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
|
||||
if (ev.target === this) return;
|
||||
|
||||
const newValue = !schema.name
|
||||
? ev.detail.value
|
||||
: { [schema.name]: ev.detail.value };
|
||||
const newValue =
|
||||
!schema.name || ("flatten" in schema && schema.flatten)
|
||||
? ev.detail.value
|
||||
: { [schema.name]: ev.detail.value };
|
||||
|
||||
this.data = {
|
||||
...this.data,
|
||||
|
@ -31,15 +31,15 @@ export interface HaFormBaseSchema {
|
||||
|
||||
export interface HaFormGridSchema extends HaFormBaseSchema {
|
||||
type: "grid";
|
||||
name: string;
|
||||
flatten?: boolean;
|
||||
column_min_width?: string;
|
||||
schema: readonly HaFormSchema[];
|
||||
}
|
||||
|
||||
export interface HaFormExpandableSchema extends HaFormBaseSchema {
|
||||
type: "expandable";
|
||||
name: "";
|
||||
title: string;
|
||||
flatten?: boolean;
|
||||
title?: string;
|
||||
icon?: string;
|
||||
iconPath?: string;
|
||||
expanded?: boolean;
|
||||
@ -100,7 +100,7 @@ export type SchemaUnion<
|
||||
SchemaArray extends readonly HaFormSchema[],
|
||||
Schema = SchemaArray[number],
|
||||
> = Schema extends HaFormGridSchema | HaFormExpandableSchema
|
||||
? SchemaUnion<Schema["schema"]>
|
||||
? SchemaUnion<Schema["schema"]> | Schema
|
||||
: Schema;
|
||||
|
||||
export interface HaFormDataContainer {
|
||||
|
@ -1,24 +1,24 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "./ha-icon-button";
|
||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
|
||||
import "./ha-icon-button";
|
||||
|
||||
import { mdiRestore } from "@mdi/js";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { HomeAssistant } from "../types";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
|
||||
type GridSizeValue = {
|
||||
rows?: number | "auto";
|
||||
columns?: number;
|
||||
};
|
||||
import {
|
||||
CardGridSize,
|
||||
DEFAULT_GRID_SIZE,
|
||||
} from "../panels/lovelace/common/compute-card-grid-size";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-grid-size-picker")
|
||||
export class HaGridSizeEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public value?: GridSizeValue;
|
||||
@property({ attribute: false }) public value?: CardGridSize;
|
||||
|
||||
@property({ attribute: false }) public rows = 8;
|
||||
|
||||
@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public isDefault?: boolean;
|
||||
|
||||
@state() public _localValue?: GridSizeValue = undefined;
|
||||
@state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
|
||||
|
||||
protected willUpdate(changedProperties) {
|
||||
if (changedProperties.has("value")) {
|
||||
@ -49,6 +49,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
this.rowMin !== undefined && this.rowMin === this.rowMax;
|
||||
|
||||
const autoHeight = this._localValue?.rows === "auto";
|
||||
const fullWidth = this._localValue?.columns === "full";
|
||||
|
||||
const rowMin = this.rowMin ?? 1;
|
||||
const rowMax = this.rowMax ?? this.rows;
|
||||
@ -67,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
.min=${columnMin}
|
||||
.max=${columnMax}
|
||||
.range=${this.columns}
|
||||
.value=${columnValue}
|
||||
.value=${fullWidth ? this.columns : columnValue}
|
||||
@value-changed=${this._valueChanged}
|
||||
@slider-moved=${this._sliderMoved}
|
||||
.disabled=${disabledColumns}
|
||||
@ -104,12 +105,12 @@ export class HaGridSizeEditor extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
<div
|
||||
class="preview"
|
||||
class="preview ${classMap({ "full-width": fullWidth })}"
|
||||
style=${styleMap({
|
||||
"--total-rows": this.rows,
|
||||
"--total-columns": this.columns,
|
||||
"--rows": rowValue,
|
||||
"--columns": columnValue,
|
||||
"--columns": fullWidth ? this.columns : columnValue,
|
||||
})}
|
||||
>
|
||||
<div>
|
||||
@ -140,12 +141,21 @@ export class HaGridSizeEditor extends LitElement {
|
||||
const cell = ev.currentTarget as HTMLElement;
|
||||
const rows = Number(cell.getAttribute("data-row"));
|
||||
const columns = Number(cell.getAttribute("data-column"));
|
||||
const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
|
||||
const clampedColumn = conditionalClamp(
|
||||
const clampedRow: CardGridSize["rows"] = conditionalClamp(
|
||||
rows,
|
||||
this.rowMin,
|
||||
this.rowMax
|
||||
);
|
||||
let clampedColumn: CardGridSize["columns"] = conditionalClamp(
|
||||
columns,
|
||||
this.columnMin,
|
||||
this.columnMax
|
||||
);
|
||||
|
||||
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
|
||||
if (currentSize.columns === "full" && clampedColumn === this.columns) {
|
||||
clampedColumn = "full";
|
||||
}
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { rows: clampedRow, columns: clampedColumn },
|
||||
});
|
||||
@ -153,12 +163,23 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
private _valueChanged(ev) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.currentTarget.id;
|
||||
const newValue = {
|
||||
...this.value,
|
||||
[key]: ev.detail.value,
|
||||
const key = ev.currentTarget.id as "rows" | "columns";
|
||||
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
|
||||
let value = ev.detail.value as CardGridSize[typeof key];
|
||||
|
||||
if (
|
||||
key === "columns" &&
|
||||
currentSize.columns === "full" &&
|
||||
value === this.columns
|
||||
) {
|
||||
value = "full";
|
||||
}
|
||||
|
||||
const newSize = {
|
||||
...currentSize,
|
||||
[key]: value,
|
||||
};
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
fireEvent(this, "value-changed", { value: newSize });
|
||||
}
|
||||
|
||||
private _reset(ev) {
|
||||
@ -173,11 +194,14 @@ export class HaGridSizeEditor extends LitElement {
|
||||
|
||||
private _sliderMoved(ev) {
|
||||
ev.stopPropagation();
|
||||
const key = ev.currentTarget.id;
|
||||
const value = ev.detail.value;
|
||||
const key = ev.currentTarget.id as "rows" | "columns";
|
||||
const currentSize = this.value ?? DEFAULT_GRID_SIZE;
|
||||
const value = ev.detail.value as CardGridSize[typeof key] | undefined;
|
||||
|
||||
if (value === undefined) return;
|
||||
|
||||
this._localValue = {
|
||||
...this.value,
|
||||
...currentSize,
|
||||
[key]: ev.detail.value,
|
||||
};
|
||||
}
|
||||
@ -189,7 +213,7 @@ export class HaGridSizeEditor extends LitElement {
|
||||
grid-template-areas:
|
||||
"reset column-slider"
|
||||
"row-slider preview";
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-rows: auto auto;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
@ -205,17 +229,12 @@ export class HaGridSizeEditor extends LitElement {
|
||||
.preview {
|
||||
position: relative;
|
||||
grid-area: preview;
|
||||
aspect-ratio: 1 / 1.2;
|
||||
}
|
||||
.preview > div {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--total-columns), 1fr);
|
||||
grid-template-rows: repeat(var(--total-rows), 1fr);
|
||||
grid-template-rows: repeat(var(--total-rows), 25px);
|
||||
gap: 4px;
|
||||
}
|
||||
.preview .cell {
|
||||
@ -226,15 +245,23 @@ export class HaGridSizeEditor extends LitElement {
|
||||
opacity: 0.2;
|
||||
cursor: pointer;
|
||||
}
|
||||
.selected {
|
||||
.preview .selected {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.selected .cell {
|
||||
background-color: var(--primary-color);
|
||||
grid-column: 1 / span var(--columns, 0);
|
||||
grid-row: 1 / span var(--rows, 0);
|
||||
grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
|
||||
grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
|
||||
opacity: 0.5;
|
||||
}
|
||||
.preview.full-width .selected .cell {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -28,6 +28,11 @@ const LAWN_MOWER_ACTIONS: Partial<
|
||||
service: "start_mowing",
|
||||
feature: LawnMowerEntityFeature.START_MOWING,
|
||||
},
|
||||
returning: {
|
||||
action: "pause",
|
||||
service: "pause",
|
||||
feature: LawnMowerEntityFeature.PAUSE,
|
||||
},
|
||||
paused: {
|
||||
action: "resume_mowing",
|
||||
service: "start_mowing",
|
||||
|
@ -162,8 +162,14 @@ export class HaLocationSelector extends LitElement {
|
||||
|
||||
private _computeLabel = (
|
||||
entry: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(`ui.components.selectors.location.${entry.name}`);
|
||||
): string => {
|
||||
if (entry.name) {
|
||||
return this.hass.localize(
|
||||
`ui.components.selectors.location.${entry.name}`
|
||||
);
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
ha-locations-editor {
|
||||
|
@ -81,15 +81,16 @@ export class HaTargetSelector extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this._createDomains}
|
||||
></ha-target-picker>`;
|
||||
return html` ${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-target-picker
|
||||
.hass=${this.hass}
|
||||
.value=${this.value}
|
||||
.helper=${this.helper}
|
||||
.deviceFilter=${this._filterDevices}
|
||||
.entityFilter=${this._filterEntities}
|
||||
.disabled=${this.disabled}
|
||||
.createDomains=${this._createDomains}
|
||||
></ha-target-picker>`;
|
||||
}
|
||||
|
||||
private _filterEntities = (entity: HassEntity): boolean => {
|
||||
|
@ -30,7 +30,7 @@ export class HaTimeSelector extends LitElement {
|
||||
clearable
|
||||
.helper=${this.helper}
|
||||
.label=${this.label}
|
||||
enable-second
|
||||
.enableSecond=${!this.selector.time?.no_second}
|
||||
></ha-time-input>
|
||||
`;
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ import "./ha-service-picker";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
import "./ha-service-section-icon";
|
||||
|
||||
const attributeFilter = (values: any[], attribute: any) => {
|
||||
if (typeof attribute === "object") {
|
||||
@ -496,7 +497,18 @@ export class HaServiceControl extends LitElement {
|
||||
) ||
|
||||
dataField.name ||
|
||||
dataField.key}
|
||||
.secondary=${this._getSectionDescription(
|
||||
dataField,
|
||||
domain,
|
||||
serviceName
|
||||
)}
|
||||
>
|
||||
<ha-service-section-icon
|
||||
slot="icons"
|
||||
.hass=${this.hass}
|
||||
.service=${this._value!.action}
|
||||
.section=${dataField.key}
|
||||
></ha-service-section-icon>
|
||||
${Object.entries(dataField.fields).map(([key, field]) =>
|
||||
this._renderField(
|
||||
{ key, ...field },
|
||||
@ -517,6 +529,16 @@ export class HaServiceControl extends LitElement {
|
||||
)} `;
|
||||
}
|
||||
|
||||
private _getSectionDescription(
|
||||
dataField: ExtHassService["fields"][number],
|
||||
domain: string | undefined,
|
||||
serviceName: string | undefined
|
||||
) {
|
||||
return this.hass!.localize(
|
||||
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
|
||||
);
|
||||
}
|
||||
|
||||
private _renderField = (
|
||||
dataField: ExtHassService["fields"][number],
|
||||
hasOptional: boolean,
|
||||
|
53
src/components/ha-service-section-icon.ts
Normal file
53
src/components/ha-service-section-icon.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
import { serviceSectionIcon } from "../data/icons";
|
||||
|
||||
@customElement("ha-service-section-icon")
|
||||
export class HaServiceSectionIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public service?: string;
|
||||
|
||||
@property() public section?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
}
|
||||
|
||||
if (!this.service || !this.section) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceSectionIcon(this.hass, this.service, this.section).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
}
|
||||
);
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
return nothing;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-service-section-icon": HaServiceSectionIcon;
|
||||
}
|
||||
}
|
@ -16,11 +16,10 @@ import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
const NAME_MAP = { cloud: "Home Assistant Cloud" };
|
||||
|
||||
@customElement("ha-stt-picker")
|
||||
export class HaSTTPicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
@ -41,13 +40,32 @@ export class HaSTTPicker extends LitElement {
|
||||
if (!this._engines) {
|
||||
return nothing;
|
||||
}
|
||||
const value =
|
||||
this.value ??
|
||||
(this.required
|
||||
? this._engines.find(
|
||||
(engine) => engine.supported_languages?.length !== 0
|
||||
)
|
||||
: NONE);
|
||||
|
||||
let value = this.value;
|
||||
if (!value && this.required) {
|
||||
for (const entity of Object.values(this.hass.entities)) {
|
||||
if (
|
||||
entity.platform === "cloud" &&
|
||||
computeDomain(entity.entity_id) === "stt"
|
||||
) {
|
||||
value = entity.entity_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
for (const sttEngine of this._engines) {
|
||||
if (sttEngine?.supported_languages?.length !== 0) {
|
||||
value = sttEngine.engine_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!value) {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@ -66,12 +84,15 @@ export class HaSTTPicker extends LitElement {
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._engines.map((engine) => {
|
||||
let label = engine.engine_id;
|
||||
if (engine.deprecated && engine.engine_id !== value) {
|
||||
return nothing;
|
||||
}
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else if (engine.engine_id in NAME_MAP) {
|
||||
label = NAME_MAP[engine.engine_id];
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return html`<ha-list-item
|
||||
.value=${engine.engine_id}
|
||||
|
@ -16,14 +16,10 @@ import { HomeAssistant } from "../types";
|
||||
import "./ha-list-item";
|
||||
import "./ha-select";
|
||||
import type { HaSelect } from "./ha-select";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
|
||||
const NONE = "__NONE_OPTION__";
|
||||
|
||||
const NAME_MAP = {
|
||||
cloud: "Home Assistant Cloud",
|
||||
google_translate: "Google Translate",
|
||||
};
|
||||
|
||||
@customElement("ha-tts-picker")
|
||||
export class HaTTSPicker extends LitElement {
|
||||
@property() public value?: string;
|
||||
@ -44,13 +40,32 @@ export class HaTTSPicker extends LitElement {
|
||||
if (!this._engines) {
|
||||
return nothing;
|
||||
}
|
||||
const value =
|
||||
this.value ??
|
||||
(this.required
|
||||
? this._engines.find(
|
||||
(engine) => engine.supported_languages?.length !== 0
|
||||
)
|
||||
: NONE);
|
||||
|
||||
let value = this.value;
|
||||
if (!value && this.required) {
|
||||
for (const entity of Object.values(this.hass.entities)) {
|
||||
if (
|
||||
entity.platform === "cloud" &&
|
||||
computeDomain(entity.entity_id) === "tts"
|
||||
) {
|
||||
value = entity.entity_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
for (const ttsEngine of this._engines) {
|
||||
if (ttsEngine?.supported_languages?.length !== 0) {
|
||||
value = ttsEngine.engine_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!value) {
|
||||
value = NONE;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
.label=${this.label ||
|
||||
@ -69,12 +84,15 @@ export class HaTTSPicker extends LitElement {
|
||||
</ha-list-item>`
|
||||
: nothing}
|
||||
${this._engines.map((engine) => {
|
||||
let label = engine.engine_id;
|
||||
if (engine.deprecated && engine.engine_id !== value) {
|
||||
return nothing;
|
||||
}
|
||||
let label: string;
|
||||
if (engine.engine_id.includes(".")) {
|
||||
const stateObj = this.hass!.states[engine.engine_id];
|
||||
label = stateObj ? computeStateName(stateObj) : engine.engine_id;
|
||||
} else if (engine.engine_id in NAME_MAP) {
|
||||
label = NAME_MAP[engine.engine_id];
|
||||
} else {
|
||||
label = engine.name || engine.engine_id;
|
||||
}
|
||||
return html`<ha-list-item
|
||||
.value=${engine.engine_id}
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
isLastDayOfMonth,
|
||||
} from "date-fns";
|
||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
calcDate,
|
||||
calcDateProperty,
|
||||
@ -791,3 +792,147 @@ export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
|
||||
|
||||
export const energyStatisticHelpUrl =
|
||||
"/docs/energy/faq/#troubleshooting-missing-entities";
|
||||
|
||||
interface EnergySumData {
|
||||
to_grid?: { [start: number]: number };
|
||||
from_grid?: { [start: number]: number };
|
||||
to_battery?: { [start: number]: number };
|
||||
from_battery?: { [start: number]: number };
|
||||
solar?: { [start: number]: number };
|
||||
}
|
||||
|
||||
interface EnergyConsumptionData {
|
||||
total: { [start: number]: number };
|
||||
}
|
||||
|
||||
export const getSummedData = memoizeOne(
|
||||
(
|
||||
data: EnergyData
|
||||
): { summedData: EnergySumData; compareSummedData?: EnergySumData } => {
|
||||
const summedData = getSummedDataPartial(data);
|
||||
const compareSummedData = data.statsCompare
|
||||
? getSummedDataPartial(data, true)
|
||||
: undefined;
|
||||
return { summedData, compareSummedData };
|
||||
}
|
||||
);
|
||||
|
||||
const getSummedDataPartial = (
|
||||
data: EnergyData,
|
||||
compare?: boolean
|
||||
): EnergySumData => {
|
||||
const statIds: {
|
||||
to_grid?: string[];
|
||||
from_grid?: string[];
|
||||
solar?: string[];
|
||||
to_battery?: string[];
|
||||
from_battery?: string[];
|
||||
} = {};
|
||||
|
||||
for (const source of data.prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
if (statIds.solar) {
|
||||
statIds.solar.push(source.stat_energy_from);
|
||||
} else {
|
||||
statIds.solar = [source.stat_energy_from];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type === "battery") {
|
||||
if (statIds.to_battery) {
|
||||
statIds.to_battery.push(source.stat_energy_to);
|
||||
statIds.from_battery!.push(source.stat_energy_from);
|
||||
} else {
|
||||
statIds.to_battery = [source.stat_energy_to];
|
||||
statIds.from_battery = [source.stat_energy_from];
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (source.type !== "grid") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// grid source
|
||||
for (const flowFrom of source.flow_from) {
|
||||
if (statIds.from_grid) {
|
||||
statIds.from_grid.push(flowFrom.stat_energy_from);
|
||||
} else {
|
||||
statIds.from_grid = [flowFrom.stat_energy_from];
|
||||
}
|
||||
}
|
||||
for (const flowTo of source.flow_to) {
|
||||
if (statIds.to_grid) {
|
||||
statIds.to_grid.push(flowTo.stat_energy_to);
|
||||
} else {
|
||||
statIds.to_grid = [flowTo.stat_energy_to];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const summedData: EnergySumData = {};
|
||||
Object.entries(statIds).forEach(([key, subStatIds]) => {
|
||||
const totalStats: { [start: number]: number } = {};
|
||||
const sets: { [statId: string]: { [start: number]: number } } = {};
|
||||
subStatIds!.forEach((id) => {
|
||||
const stats = compare ? data.statsCompare[id] : data.stats[id];
|
||||
if (!stats) {
|
||||
return;
|
||||
}
|
||||
|
||||
const set = {};
|
||||
stats.forEach((stat) => {
|
||||
if (stat.change === null || stat.change === undefined) {
|
||||
return;
|
||||
}
|
||||
const val = stat.change;
|
||||
// Get total of solar and to grid to calculate the solar energy used
|
||||
totalStats[stat.start] =
|
||||
stat.start in totalStats ? totalStats[stat.start] + val : val;
|
||||
});
|
||||
sets[id] = set;
|
||||
});
|
||||
summedData[key] = totalStats;
|
||||
});
|
||||
|
||||
return summedData;
|
||||
};
|
||||
|
||||
export const computeConsumptionData = memoizeOne(
|
||||
(
|
||||
data: EnergySumData,
|
||||
compareData?: EnergySumData
|
||||
): {
|
||||
consumption: EnergyConsumptionData;
|
||||
compareConsumption?: EnergyConsumptionData;
|
||||
} => {
|
||||
const consumption = computeConsumptionDataPartial(data);
|
||||
const compareConsumption = compareData
|
||||
? computeConsumptionDataPartial(compareData)
|
||||
: undefined;
|
||||
return { consumption, compareConsumption };
|
||||
}
|
||||
);
|
||||
|
||||
const computeConsumptionDataPartial = (
|
||||
data: EnergySumData
|
||||
): EnergyConsumptionData => {
|
||||
const outData: EnergyConsumptionData = { total: {} };
|
||||
|
||||
Object.keys(data).forEach((type) => {
|
||||
Object.keys(data[type]).forEach((start) => {
|
||||
if (outData.total[start] === undefined) {
|
||||
const consumption =
|
||||
(data.from_grid?.[start] || 0) +
|
||||
(data.solar?.[start] || 0) +
|
||||
(data.from_battery?.[start] || 0) -
|
||||
(data.to_grid?.[start] || 0) -
|
||||
(data.to_battery?.[start] || 0);
|
||||
outData.total[start] = consumption;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return outData;
|
||||
};
|
||||
|
@ -62,7 +62,7 @@ export interface ComponentIcons {
|
||||
}
|
||||
|
||||
interface ServiceIcons {
|
||||
[service: string]: string;
|
||||
[service: string]: { service: string; sections?: { [name: string]: string } };
|
||||
}
|
||||
|
||||
export type IconCategory = "entity" | "entity_component" | "services";
|
||||
@ -288,7 +288,8 @@ export const serviceIcon = async (
|
||||
const serviceName = computeObjectId(service);
|
||||
const serviceIcons = await getServiceIcons(hass, domain);
|
||||
if (serviceIcons) {
|
||||
icon = serviceIcons[serviceName] as string;
|
||||
const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
|
||||
icon = srvceIcon?.service;
|
||||
}
|
||||
if (!icon) {
|
||||
icon = await domainIcon(hass, domain);
|
||||
@ -296,6 +297,21 @@ export const serviceIcon = async (
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const serviceSectionIcon = async (
|
||||
hass: HomeAssistant,
|
||||
service: string,
|
||||
section: string
|
||||
): Promise<string | undefined> => {
|
||||
const domain = computeDomain(service);
|
||||
const serviceName = computeObjectId(service);
|
||||
const serviceIcons = await getServiceIcons(hass, domain);
|
||||
if (serviceIcons) {
|
||||
const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
|
||||
return srvceIcon?.sections?.[section];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const domainIcon = async (
|
||||
hass: HomeAssistant,
|
||||
domain: string,
|
||||
|
@ -4,7 +4,12 @@ import {
|
||||
} from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE } from "./entity";
|
||||
|
||||
export type LawnMowerEntityState = "paused" | "mowing" | "docked" | "error";
|
||||
export type LawnMowerEntityState =
|
||||
| "paused"
|
||||
| "mowing"
|
||||
| "returning"
|
||||
| "docked"
|
||||
| "error";
|
||||
|
||||
export const enum LawnMowerEntityFeature {
|
||||
START_MOWING = 1,
|
||||
|
@ -13,7 +13,7 @@ export const ensureBadgeConfig = (
|
||||
return {
|
||||
type: "entity",
|
||||
entity: config,
|
||||
display_type: "complete",
|
||||
show_name: true,
|
||||
};
|
||||
}
|
||||
if ("type" in config && config.type) {
|
||||
|
@ -5,6 +5,7 @@ import type { LovelaceStrategyConfig } from "./strategy";
|
||||
export interface LovelaceBaseSectionConfig {
|
||||
title?: string;
|
||||
visibility?: Condition[];
|
||||
column_span?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
|
||||
|
@ -427,8 +427,7 @@ export interface ThemeSelector {
|
||||
theme: { include_default?: boolean } | null;
|
||||
}
|
||||
export interface TimeSelector {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
time: {} | null;
|
||||
time: { no_second?: boolean } | null;
|
||||
}
|
||||
|
||||
export interface TriggerSelector {
|
||||
|
@ -21,6 +21,8 @@ export interface SpeechMetadata {
|
||||
export interface STTEngine {
|
||||
engine_id: string;
|
||||
supported_languages?: string[];
|
||||
name?: string;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
export const listSTTEngines = (
|
||||
|
@ -3,6 +3,8 @@ import { HomeAssistant } from "../types";
|
||||
export interface TTSEngine {
|
||||
engine_id: string;
|
||||
supported_languages?: string[];
|
||||
name?: string;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
export interface TTSVoice {
|
||||
|
@ -76,17 +76,36 @@ export const showConfigFlowDialog = (
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field) {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.data.${field.name}`
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}` : "";
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${step.handler}.config.step.${step.step_id}.${prefix}data.${field.name}`
|
||||
) || field.name
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field) {
|
||||
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.sections.${field.name}.description`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.data_description.${field.name}`,
|
||||
`component.${step.translation_domain || step.handler}.config.step.${step.step_id}.${prefix}data_description.${field.name}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
|
||||
return description
|
||||
? html`<ha-markdown breaks .content=${description}></ha-markdown>`
|
||||
: "";
|
||||
|
@ -49,13 +49,15 @@ export interface FlowConfig {
|
||||
renderShowFormStepFieldLabel(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm,
|
||||
field: HaFormSchema
|
||||
field: HaFormSchema,
|
||||
options: { path?: string[]; [key: string]: any }
|
||||
): string;
|
||||
|
||||
renderShowFormStepFieldHelper(
|
||||
hass: HomeAssistant,
|
||||
step: DataEntryFlowStepForm,
|
||||
field: HaFormSchema
|
||||
field: HaFormSchema,
|
||||
options: { path?: string[]; [key: string]: any }
|
||||
): TemplateResult | string;
|
||||
|
||||
renderShowFormStepFieldError(
|
||||
|
@ -93,15 +93,33 @@ export const showOptionsFlowDialog = (
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field) {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.data.${field.name}`
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${configEntry.domain}.options.step.${step.step_id}.${prefix}data.${field.name}`
|
||||
) || field.name
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field) {
|
||||
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||
if (field.type === "expandable") {
|
||||
return hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.description`
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
|
||||
|
||||
const description = hass.localize(
|
||||
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`,
|
||||
`component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.${prefix}data_description.${field.name}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
|
@ -225,11 +225,24 @@ class StepFlowForm extends LitElement {
|
||||
this._stepData = ev.detail.value;
|
||||
}
|
||||
|
||||
private _labelCallback = (field: HaFormSchema): string =>
|
||||
this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field);
|
||||
private _labelCallback = (field: HaFormSchema, _data, options): string =>
|
||||
this.flowConfig.renderShowFormStepFieldLabel(
|
||||
this.hass,
|
||||
this.step,
|
||||
field,
|
||||
options
|
||||
);
|
||||
|
||||
private _helperCallback = (field: HaFormSchema): string | TemplateResult =>
|
||||
this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field);
|
||||
private _helperCallback = (
|
||||
field: HaFormSchema,
|
||||
options
|
||||
): string | TemplateResult =>
|
||||
this.flowConfig.renderShowFormStepFieldHelper(
|
||||
this.hass,
|
||||
this.step,
|
||||
field,
|
||||
options
|
||||
);
|
||||
|
||||
private _errorCallback = (error: string) =>
|
||||
this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);
|
||||
|
@ -140,10 +140,12 @@ export class HaVoiceCommandDialog extends LitElement {
|
||||
|
||||
const controlHA = !this._pipeline
|
||||
? false
|
||||
: supportsFeature(
|
||||
this.hass.states[this._pipeline?.conversation_engine],
|
||||
ConversationEntityFeature.CONTROL
|
||||
);
|
||||
: this.hass.states[this._pipeline?.conversation_engine]
|
||||
? supportsFeature(
|
||||
this.hass.states[this._pipeline?.conversation_engine],
|
||||
ConversationEntityFeature.CONTROL
|
||||
)
|
||||
: true;
|
||||
const supportsMicrophone = AudioRecorder.isSupported;
|
||||
const supportsSTT = this._pipeline?.stt_engine;
|
||||
|
||||
|
@ -29,7 +29,6 @@ const COMPONENTS = {
|
||||
history: () => import("../panels/history/ha-panel-history"),
|
||||
iframe: () => import("../panels/iframe/ha-panel-iframe"),
|
||||
logbook: () => import("../panels/logbook/ha-panel-logbook"),
|
||||
mailbox: () => import("../panels/mailbox/ha-panel-mailbox"),
|
||||
map: () => import("../panels/map/ha-panel-map"),
|
||||
my: () => import("../panels/my/ha-panel-my"),
|
||||
profile: () => import("../panels/profile/ha-panel-profile"),
|
||||
|
@ -1,29 +0,0 @@
|
||||
const documentContainer = document.createElement("template");
|
||||
documentContainer.setAttribute("style", "display: none;");
|
||||
|
||||
documentContainer.innerHTML = `<dom-module id="ha-form-style">
|
||||
<template>
|
||||
<style>
|
||||
.form-group {
|
||||
@apply --layout-horizontal;
|
||||
@apply --layout-center;
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
@apply --layout-flex-2;
|
||||
}
|
||||
|
||||
.form-group .form-control {
|
||||
@apply --layout-flex;
|
||||
}
|
||||
|
||||
.form-group.vertical {
|
||||
@apply --layout-vertical;
|
||||
@apply --layout-start;
|
||||
}
|
||||
</style>
|
||||
</template>
|
||||
</dom-module>`;
|
||||
|
||||
document.head.appendChild(documentContainer.content);
|
@ -51,6 +51,7 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant, Route } from "../../types";
|
||||
import { subscribeLabelRegistry } from "../../data/label_registry";
|
||||
import { subscribeFloorRegistry } from "../../data/floor_registry";
|
||||
import { throttle } from "../../common/util/throttle";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@ -395,6 +396,10 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
||||
initialValue: [],
|
||||
});
|
||||
|
||||
private _hassThrottler = throttle((el, hass) => {
|
||||
el.hass = hass;
|
||||
}, 1000);
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
subscribeEntityRegistry(this.hass.connection!, (entities) => {
|
||||
@ -641,7 +646,11 @@ class HaPanelConfig extends SubscribeMixin(HassRouterPage) {
|
||||
this.hass.dockedSidebar === "docked" ? this._wideSidebar : this._wide;
|
||||
|
||||
el.route = this.routeTail;
|
||||
el.hass = this.hass;
|
||||
if (el.hass !== undefined) {
|
||||
this._hassThrottler(el, this.hass);
|
||||
} else {
|
||||
el.hass = this.hass;
|
||||
}
|
||||
el.showAdvanced = Boolean(this.hass.userData?.showAdvanced);
|
||||
el.isWide = isWide;
|
||||
el.narrow = this.narrow;
|
||||
|
133
src/panels/config/helpers/forms/dialog-schedule-block-info.ts
Normal file
133
src/panels/config/helpers/forms/dialog-schedule-block-info.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import "../../../../components/ha-button";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
ScheduleBlockInfo,
|
||||
ScheduleBlockInfoDialogParams,
|
||||
} from "./show-dialog-schedule-block-info";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "from",
|
||||
required: true,
|
||||
selector: { time: { no_second: true } },
|
||||
},
|
||||
{
|
||||
name: "to",
|
||||
required: true,
|
||||
selector: { time: { no_second: true } },
|
||||
},
|
||||
];
|
||||
|
||||
class DialogScheduleBlockInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _error?: Record<string, string>;
|
||||
|
||||
@state() private _data?: ScheduleBlockInfo;
|
||||
|
||||
@state() private _params?: ScheduleBlockInfoDialogParams;
|
||||
|
||||
public showDialog(params: ScheduleBlockInfoDialogParams): void {
|
||||
this._params = params;
|
||||
this._error = undefined;
|
||||
this._data = params.block;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._params = undefined;
|
||||
this._data = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._params || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass!.localize(
|
||||
"ui.dialogs.helper_settings.schedule.edit_schedule_block"
|
||||
)
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.schema=${SCHEMA}
|
||||
.data=${this._data}
|
||||
.error=${this._error}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
</div>
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
class="warning"
|
||||
@click=${this._deleteBlock}
|
||||
>
|
||||
${this.hass!.localize("ui.common.delete")}
|
||||
</ha-button>
|
||||
<ha-button slot="primaryAction" @click=${this._updateBlock}>
|
||||
${this.hass!.localize("ui.common.save")}
|
||||
</ha-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
this._error = undefined;
|
||||
this._data = ev.detail.value;
|
||||
}
|
||||
|
||||
private _updateBlock() {
|
||||
try {
|
||||
this._params!.updateBlock!(this._data!);
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = { base: err ? err.message : "Unknown error" };
|
||||
}
|
||||
}
|
||||
|
||||
private _deleteBlock() {
|
||||
try {
|
||||
this._params!.deleteBlock!();
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
this._error = { base: err ? err.message : "Unknown error" };
|
||||
}
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
switch (schema.name) {
|
||||
case "from":
|
||||
return this.hass!.localize("ui.dialogs.helper_settings.schedule.start");
|
||||
case "to":
|
||||
return this.hass!.localize("ui.dialogs.helper_settings.schedule.end");
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [haStyleDialog];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-schedule-block-info": DialogScheduleBlockInfo;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("dialog-schedule-block-info", DialogScheduleBlockInfo);
|
@ -20,7 +20,7 @@ import "../../../../components/ha-icon-picker";
|
||||
import "../../../../components/ha-textfield";
|
||||
import { Schedule, ScheduleDay, weekdays } from "../../../../data/schedule";
|
||||
import { TimeZone } from "../../../../data/translation";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
@ -352,21 +352,34 @@ class HaScheduleForm extends LitElement {
|
||||
}
|
||||
|
||||
private async _handleEventClick(info: any) {
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass.localize("ui.dialogs.helper_settings.schedule.delete"),
|
||||
text: this.hass.localize(
|
||||
"ui.dialogs.helper_settings.schedule.confirm_delete"
|
||||
),
|
||||
destructive: true,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
}))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const [day, index] = info.event.id.split("-");
|
||||
const value = [...this[`_${day}`]];
|
||||
const item = [...this[`_${day}`]][index];
|
||||
showScheduleBlockInfoDialog(this, {
|
||||
block: item,
|
||||
updateBlock: (newBlock) => this._updateBlock(day, index, newBlock),
|
||||
deleteBlock: () => this._deleteBlock(day, index),
|
||||
});
|
||||
}
|
||||
|
||||
private _updateBlock(day, index, newBlock) {
|
||||
const [fromH, fromM, _fromS] = newBlock.from.split(":");
|
||||
newBlock.from = `${fromH}:${fromM}`;
|
||||
const [toH, toM, _toS] = newBlock.to.split(":");
|
||||
newBlock.to = `${toH}:${toM}`;
|
||||
if (Number(toH) === 0 && Number(toM) === 0) {
|
||||
newBlock.to = "24:00";
|
||||
}
|
||||
const newValue = { ...this._item };
|
||||
newValue[day] = [...this._item![day]];
|
||||
newValue[day][index] = newBlock;
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: newValue,
|
||||
});
|
||||
}
|
||||
|
||||
private _deleteBlock(day, index) {
|
||||
const value = [...this[`_${day}`]];
|
||||
const newValue = { ...this._item };
|
||||
value.splice(parseInt(index), 1);
|
||||
newValue[day] = value;
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
export interface ScheduleBlockInfo {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface ScheduleBlockInfoDialogParams {
|
||||
block: ScheduleBlockInfo;
|
||||
updateBlock?: (update: ScheduleBlockInfo) => void;
|
||||
deleteBlock?: () => void;
|
||||
}
|
||||
|
||||
export const loadScheduleBlockInfoDialog = () =>
|
||||
import("./dialog-schedule-block-info");
|
||||
|
||||
export const showScheduleBlockInfoDialog = (
|
||||
element: HTMLElement,
|
||||
params: ScheduleBlockInfoDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-schedule-block-info",
|
||||
dialogImport: loadScheduleBlockInfoDialog,
|
||||
dialogParams: params,
|
||||
});
|
||||
};
|
@ -8,6 +8,7 @@ import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"
|
||||
import { createCloseHeading } from "../../../../../components/ha-dialog";
|
||||
import { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../components/buttons/ha-progress-button";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-select";
|
||||
import "../../../../../components/ha-list-item";
|
||||
@ -70,10 +71,22 @@ class DialogZHAChangeChannel extends LitElement implements HassDialog {
|
||||
this.hass.localize("ui.panel.config.zha.change_channel_dialog.title")
|
||||
)}
|
||||
>
|
||||
<p>
|
||||
<ha-alert alert-type="warning">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.migration_warning"
|
||||
)}
|
||||
</ha-alert>
|
||||
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.description"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.smart_explanation"
|
||||
)}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
@ -90,7 +103,11 @@ class DialogZHAChangeChannel extends LitElement implements HassDialog {
|
||||
${VALID_CHANNELS.map(
|
||||
(newChannel) =>
|
||||
html`<ha-list-item .value=${String(newChannel)}
|
||||
>${newChannel}</ha-list-item
|
||||
>${newChannel === "auto"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.zha.change_channel_dialog.channel_auto"
|
||||
)
|
||||
: newChannel}</ha-list-item
|
||||
>`
|
||||
)}
|
||||
</ha-select>
|
||||
|
@ -96,20 +96,20 @@ export const showRepairsFlowDialog = (
|
||||
: "";
|
||||
},
|
||||
|
||||
renderShowFormStepFieldLabel(hass, step, field) {
|
||||
renderShowFormStepFieldLabel(hass, step, field, options) {
|
||||
return hass.localize(
|
||||
`component.${issue.domain}.issues.${
|
||||
issue.translation_key || issue.issue_id
|
||||
}.fix_flow.step.${step.step_id}.data.${field.name}`,
|
||||
}.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data.${field.name}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
},
|
||||
|
||||
renderShowFormStepFieldHelper(hass, step, field) {
|
||||
renderShowFormStepFieldHelper(hass, step, field, options) {
|
||||
const description = hass.localize(
|
||||
`component.${issue.domain}.issues.${
|
||||
issue.translation_key || issue.issue_id
|
||||
}.fix_flow.step.${step.step_id}.data_description.${field.name}`,
|
||||
}.fix_flow.step.${step.step_id}.${options?.prefix ? `section.${options.prefix[0]}.` : ""}data_description.${field.name}`,
|
||||
step.description_placeholders
|
||||
);
|
||||
return description
|
||||
|
@ -28,6 +28,7 @@ import "./assist-pipeline-detail/assist-pipeline-detail-tts";
|
||||
import "./assist-pipeline-detail/assist-pipeline-detail-wakeword";
|
||||
import "./debug/assist-render-pipeline-events";
|
||||
import { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
|
||||
@customElement("dialog-voice-assistant-pipeline-detail")
|
||||
export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
@ -54,15 +55,36 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
if (this._params.pipeline) {
|
||||
this._data = this._params.pipeline;
|
||||
this._preferred = this._params.preferred;
|
||||
} else {
|
||||
this._data = {
|
||||
language: (
|
||||
this.hass.config.language || this.hass.locale.language
|
||||
).substring(0, 2),
|
||||
stt_engine: this._cloudActive ? "cloud" : undefined,
|
||||
tts_engine: this._cloudActive ? "cloud" : undefined,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
let sstDefault: string | undefined;
|
||||
let ttsDefault: string | undefined;
|
||||
if (this._cloudActive) {
|
||||
for (const entity of Object.values(this.hass.entities)) {
|
||||
if (entity.platform !== "cloud") {
|
||||
continue;
|
||||
}
|
||||
if (computeDomain(entity.entity_id) === "stt") {
|
||||
sstDefault = entity.entity_id;
|
||||
if (ttsDefault) {
|
||||
break;
|
||||
}
|
||||
} else if (computeDomain(entity.entity_id) === "tts") {
|
||||
ttsDefault = entity.entity_id;
|
||||
if (sstDefault) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._data = {
|
||||
language: (
|
||||
this.hass.config.language || this.hass.locale.language
|
||||
).substring(0, 2),
|
||||
stt_engine: sstDefault,
|
||||
tts_engine: ttsDefault,
|
||||
};
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
|
@ -308,7 +308,7 @@ class HaPanelDevAction extends LitElement {
|
||||
|
||||
private async _copyTemplate(): Promise<void> {
|
||||
await copyToClipboard(
|
||||
`{% set action_response = ${JSON.stringify(this._response)} %}`
|
||||
`{% set ${this._serviceData?.response_variable || "action_response"} = ${JSON.stringify(this._response)} %}`
|
||||
);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.common.copied_clipboard"),
|
||||
|
@ -88,13 +88,8 @@ class HaPanelDevTemplate extends LitElement {
|
||||
: "dict"
|
||||
: type;
|
||||
return html`
|
||||
<div
|
||||
class="content ${classMap({
|
||||
layout: !this.narrow,
|
||||
horizontal: !this.narrow,
|
||||
})}"
|
||||
>
|
||||
<div class="edit-pane">
|
||||
<div class="content">
|
||||
<div class="description">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.description"
|
||||
@ -126,123 +121,143 @@ class HaPanelDevTemplate extends LitElement {
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.editor"
|
||||
)}
|
||||
</p>
|
||||
<ha-code-editor
|
||||
mode="jinja2"
|
||||
.hass=${this.hass}
|
||||
.value=${this._template}
|
||||
.error=${this._error}
|
||||
autofocus
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
@value-changed=${this._templateChanged}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
<mwc-button @click=${this._restoreDemo}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.reset"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._clear}>
|
||||
${this.hass.localize("ui.common.clear")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="content ${classMap({
|
||||
layout: !this.narrow,
|
||||
horizontal: !this.narrow,
|
||||
})}"
|
||||
>
|
||||
<ha-card
|
||||
class="edit-pane"
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.editor"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-code-editor
|
||||
mode="jinja2"
|
||||
.hass=${this.hass}
|
||||
.value=${this._template}
|
||||
.error=${this._error}
|
||||
autofocus
|
||||
autocomplete-entities
|
||||
autocomplete-icons
|
||||
@value-changed=${this._templateChanged}
|
||||
dir="ltr"
|
||||
></ha-code-editor>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<mwc-button @click=${this._restoreDemo}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.reset"
|
||||
)}
|
||||
</mwc-button>
|
||||
<mwc-button @click=${this._clear}>
|
||||
${this.hass.localize("ui.common.clear")}
|
||||
</mwc-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<div class="render-pane">
|
||||
${this._rendering
|
||||
? html`<ha-circular-progress
|
||||
class="render-spinner"
|
||||
indeterminate
|
||||
size="small"
|
||||
></ha-circular-progress>`
|
||||
: ""}
|
||||
${this._error
|
||||
? html`<ha-alert
|
||||
alert-type=${this._errorLevel?.toLowerCase() || "error"}
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: nothing}
|
||||
${this._templateResult
|
||||
? html`${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.result_type"
|
||||
)}:
|
||||
${resultType}
|
||||
<!-- prettier-ignore -->
|
||||
<pre class="rendered ${classMap({
|
||||
[resultType]: resultType,
|
||||
})}"
|
||||
>${type === "object"
|
||||
? JSON.stringify(this._templateResult.result, null, 2)
|
||||
: this._templateResult.result}</pre>
|
||||
${this._templateResult.listeners.time
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.time"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: ""}
|
||||
${!this._templateResult.listeners
|
||||
? nothing
|
||||
: this._templateResult.listeners.all
|
||||
<ha-card
|
||||
class="render-pane"
|
||||
header=${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.result"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._rendering
|
||||
? html`<ha-circular-progress
|
||||
class="render-spinner"
|
||||
indeterminate
|
||||
size="small"
|
||||
></ha-circular-progress>`
|
||||
: ""}
|
||||
${this._error
|
||||
? html`<ha-alert
|
||||
alert-type=${this._errorLevel?.toLowerCase() || "error"}
|
||||
>${this._error}</ha-alert
|
||||
>`
|
||||
: nothing}
|
||||
${this._templateResult
|
||||
? html`${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.result_type"
|
||||
)}:
|
||||
${resultType}
|
||||
<!-- prettier-ignore -->
|
||||
<pre class="rendered ${classMap({
|
||||
[resultType]: resultType,
|
||||
})}"
|
||||
>${type === "object"
|
||||
? JSON.stringify(this._templateResult.result, null, 2)
|
||||
: this._templateResult.result}</pre>
|
||||
${this._templateResult.listeners.time
|
||||
? html`
|
||||
<p class="all_listeners">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.all_listeners"
|
||||
"ui.panel.developer-tools.tabs.templates.time"
|
||||
)}
|
||||
</p>
|
||||
`
|
||||
: this._templateResult.listeners.domains.length ||
|
||||
this._templateResult.listeners.entities.length
|
||||
: ""}
|
||||
${!this._templateResult.listeners
|
||||
? nothing
|
||||
: this._templateResult.listeners.all
|
||||
? html`
|
||||
<p>
|
||||
<p class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.listeners"
|
||||
"ui.panel.developer-tools.tabs.templates.all_listeners"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
${this._templateResult.listeners.domains
|
||||
.sort()
|
||||
.map(
|
||||
(domain) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.domain"
|
||||
)}</b
|
||||
>: ${domain}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
${this._templateResult.listeners.entities
|
||||
.sort()
|
||||
.map(
|
||||
(entity_id) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.entity"
|
||||
)}</b
|
||||
>: ${entity_id}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: !this._templateResult.listeners.time
|
||||
? html`<span class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.no_listeners"
|
||||
)}
|
||||
</span>`
|
||||
: nothing}`
|
||||
: nothing}
|
||||
</div>
|
||||
: this._templateResult.listeners.domains.length ||
|
||||
this._templateResult.listeners.entities.length
|
||||
? html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.listeners"
|
||||
)}
|
||||
</p>
|
||||
<ul>
|
||||
${this._templateResult.listeners.domains
|
||||
.sort()
|
||||
.map(
|
||||
(domain) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.domain"
|
||||
)}</b
|
||||
>: ${domain}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
${this._templateResult.listeners.entities
|
||||
.sort()
|
||||
.map(
|
||||
(entity_id) => html`
|
||||
<li>
|
||||
<b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.entity"
|
||||
)}</b
|
||||
>: ${entity_id}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>
|
||||
`
|
||||
: !this._templateResult.listeners.time
|
||||
? html`<span class="all_listeners">
|
||||
${this.hass.localize(
|
||||
"ui.panel.developer-tools.tabs.templates.no_listeners"
|
||||
)}
|
||||
</span>`
|
||||
: nothing}`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@ -258,6 +273,7 @@ class HaPanelDevTemplate extends LitElement {
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
padding: max(16px, env(safe-area-inset-top))
|
||||
max(16px, env(safe-area-inset-right))
|
||||
@ -265,10 +281,11 @@ class HaPanelDevTemplate extends LitElement {
|
||||
max(16px, env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
ha-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.edit-pane {
|
||||
margin-right: 16px;
|
||||
margin-inline-start: initial;
|
||||
margin-inline-end: 16px;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
@ -280,12 +297,6 @@ class HaPanelDevTemplate extends LitElement {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.render-pane {
|
||||
position: relative;
|
||||
max-width: 50%;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.render-spinner {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { mdiAlertCircle } from "@mdi/js";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@ -8,10 +9,13 @@ import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
@ -20,15 +24,38 @@ import { handleAction } from "../common/handle-action";
|
||||
import { hasAction } from "../common/has-action";
|
||||
import { LovelaceBadge, LovelaceBadgeEditor } from "../types";
|
||||
import { EntityBadgeConfig } from "./types";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { cameraUrlWithWidthHeight } from "../../../data/camera";
|
||||
|
||||
export const DISPLAY_TYPES = ["minimal", "standard", "complete"] as const;
|
||||
|
||||
export type DisplayType = (typeof DISPLAY_TYPES)[number];
|
||||
|
||||
export const DEFAULT_DISPLAY_TYPE: DisplayType = "standard";
|
||||
|
||||
export const DEFAULT_CONFIG: EntityBadgeConfig = {
|
||||
type: "entity",
|
||||
show_name: false,
|
||||
show_state: true,
|
||||
show_icon: true,
|
||||
};
|
||||
|
||||
export const migrateLegacyEntityBadgeConfig = (
|
||||
config: EntityBadgeConfig
|
||||
): EntityBadgeConfig => {
|
||||
const newConfig = { ...config };
|
||||
if (config.display_type) {
|
||||
if (config.show_name === undefined) {
|
||||
if (config.display_type === "complete") {
|
||||
newConfig.show_name = true;
|
||||
}
|
||||
}
|
||||
if (config.show_state === undefined) {
|
||||
if (config.display_type === "minimal") {
|
||||
newConfig.show_state = false;
|
||||
}
|
||||
}
|
||||
delete newConfig.display_type;
|
||||
}
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
@customElement("hui-entity-badge")
|
||||
export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
public static async getConfigElement(): Promise<LovelaceBadgeEditor> {
|
||||
@ -62,7 +89,10 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
@state() protected _config?: EntityBadgeConfig;
|
||||
|
||||
public setConfig(config: EntityBadgeConfig): void {
|
||||
this._config = config;
|
||||
this._config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...migrateLegacyEntityBadgeConfig(config),
|
||||
};
|
||||
}
|
||||
|
||||
get hasAction() {
|
||||
@ -129,7 +159,17 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
|
||||
if (!stateObj) {
|
||||
return nothing;
|
||||
return html`
|
||||
<div class="badge error">
|
||||
<ha-svg-icon .hass=${this.hass} .path=${mdiAlertCircle}></ha-svg-icon>
|
||||
<span class="info">
|
||||
<span class="label">${entityId}</span>
|
||||
<span class="content">
|
||||
${this.hass.localize("ui.badge.entity.not_found")}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const active = stateActive(stateObj);
|
||||
@ -151,18 +191,25 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
|
||||
const name = this._config.name || stateObj.attributes.friendly_name;
|
||||
|
||||
const displayType = this._config.display_type || DEFAULT_DISPLAY_TYPE;
|
||||
const showState = this._config.show_state;
|
||||
const showName = this._config.show_name;
|
||||
const showIcon = this._config.show_icon;
|
||||
const showEntityPicture = this._config.show_entity_picture;
|
||||
|
||||
const imageUrl = this._config.show_entity_picture
|
||||
const imageUrl = showEntityPicture
|
||||
? this._getImageUrl(stateObj)
|
||||
: undefined;
|
||||
|
||||
const label = showState && showName ? name : undefined;
|
||||
const content = showState ? stateDisplay : showName ? name : undefined;
|
||||
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap(style)}
|
||||
class="badge ${classMap({
|
||||
active,
|
||||
[displayType]: true,
|
||||
"no-info": !showState && !showName,
|
||||
"no-icon": !showIcon,
|
||||
})}"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
@ -173,22 +220,22 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
tabindex=${ifDefined(this.hasAction ? "0" : undefined)}
|
||||
>
|
||||
<ha-ripple .disabled=${!this.hasAction}></ha-ripple>
|
||||
${imageUrl
|
||||
? html`<img src=${imageUrl} aria-hidden />`
|
||||
: html`
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.icon=${this._config.icon}
|
||||
></ha-state-icon>
|
||||
`}
|
||||
${displayType !== "minimal"
|
||||
${showIcon
|
||||
? imageUrl
|
||||
? html`<img src=${imageUrl} aria-hidden />`
|
||||
: html`
|
||||
<ha-state-icon
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.icon=${this._config.icon}
|
||||
></ha-state-icon>
|
||||
`
|
||||
: nothing}
|
||||
${content
|
||||
? html`
|
||||
<span class="content">
|
||||
${displayType === "complete"
|
||||
? html`<span class="name">${name}</span>`
|
||||
: nothing}
|
||||
<span class="state">${stateDisplay}</span>
|
||||
<span class="info">
|
||||
${label ? html`<span class="label">${name}</span>` : nothing}
|
||||
<span class="content">${content}</span>
|
||||
</span>
|
||||
`
|
||||
: nothing}
|
||||
@ -206,6 +253,9 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
--badge-color: var(--state-inactive-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.badge.error {
|
||||
--badge-color: var(--red-color);
|
||||
}
|
||||
.badge {
|
||||
position: relative;
|
||||
--ha-ripple-color: var(--badge-color);
|
||||
@ -219,14 +269,23 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
height: var(--ha-badge-size, 36px);
|
||||
min-width: var(--ha-badge-size, 36px);
|
||||
padding: 0px 8px;
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
border-radius: 18px;
|
||||
background-color: var(--card-background-color, white);
|
||||
border-radius: var(
|
||||
--ha-badge-border-radius,
|
||||
calc(var(--ha-badge-size, 36px) / 2)
|
||||
);
|
||||
background: var(
|
||||
--ha-card-background,
|
||||
var(--card-background-color, white)
|
||||
);
|
||||
-webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
|
||||
backdrop-filter: var(--ha-card-backdrop-filter, none);
|
||||
border-width: var(--ha-card-border-width, 1px);
|
||||
box-shadow: var(--ha-card-box-shadow, none);
|
||||
border-style: solid;
|
||||
border-color: var(
|
||||
--ha-card-border-color,
|
||||
@ -253,7 +312,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
.badge.active {
|
||||
--badge-color: var(--primary-color);
|
||||
}
|
||||
.content {
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@ -261,7 +320,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
padding-inline-end: 4px;
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
.name {
|
||||
.label {
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
@ -269,7 +328,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.state {
|
||||
.content {
|
||||
font-size: 12px;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
@ -277,7 +336,8 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
letter-spacing: 0.1px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-state-icon {
|
||||
ha-state-icon,
|
||||
ha-svg-icon {
|
||||
color: var(--badge-color);
|
||||
line-height: 0;
|
||||
}
|
||||
@ -288,14 +348,20 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
|
||||
object-fit: cover;
|
||||
overflow: hidden;
|
||||
}
|
||||
.badge.minimal {
|
||||
.badge.no-info {
|
||||
padding: 0;
|
||||
}
|
||||
.badge:not(.minimal) img {
|
||||
.badge:not(.no-icon) img {
|
||||
margin-left: -6px;
|
||||
margin-inline-start: -6px;
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.badge.no-icon .info {
|
||||
padding-right: 4px;
|
||||
padding-left: 4px;
|
||||
padding-inline-end: 4px;
|
||||
padding-inline-start: 4px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@ -16,10 +16,10 @@ export class HuiStateLabelBadge extends HuiEntityBadge {
|
||||
const entityBadgeConfig: EntityBadgeConfig = {
|
||||
type: "entity",
|
||||
entity: config.entity,
|
||||
display_type: config.show_name === false ? "standard" : "complete",
|
||||
show_name: config.show_name ?? true,
|
||||
};
|
||||
|
||||
this._config = entityBadgeConfig;
|
||||
super.setConfig(entityBadgeConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@ import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { LegacyStateFilter } from "../common/evaluate-filter";
|
||||
import type { Condition } from "../common/validate-condition";
|
||||
import type { EntityFilterEntityConfig } from "../entity-rows/types";
|
||||
import type { DisplayType } from "./hui-entity-badge";
|
||||
|
||||
export interface EntityFilterBadgeConfig extends LovelaceBadgeConfig {
|
||||
type: "entity-filter";
|
||||
@ -33,10 +34,16 @@ export interface EntityBadgeConfig extends LovelaceBadgeConfig {
|
||||
name?: string;
|
||||
icon?: string;
|
||||
color?: string;
|
||||
show_name?: boolean;
|
||||
show_state?: boolean;
|
||||
show_icon?: boolean;
|
||||
show_entity_picture?: boolean;
|
||||
display_type?: "minimal" | "standard" | "complete";
|
||||
state_content?: string | string[];
|
||||
tap_action?: ActionConfig;
|
||||
hold_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
/**
|
||||
* @deprecated use `show_state`, `show_name`, `icon_type`
|
||||
*/
|
||||
display_type?: DisplayType;
|
||||
}
|
||||
|
@ -96,7 +96,12 @@ class HuiAlarmModeCardFeature
|
||||
}
|
||||
|
||||
private async _setMode(mode: AlarmMode) {
|
||||
setProtectedAlarmControlPanelMode(this, this.hass!, this.stateObj!, mode);
|
||||
await setProtectedAlarmControlPanelMode(
|
||||
this,
|
||||
this.hass!,
|
||||
this.stateObj!,
|
||||
mode
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
|
@ -18,18 +18,22 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { getEnergyColor } from "./common/color";
|
||||
import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import {
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyData,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
computeConsumptionData,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
getStatisticLabel,
|
||||
Statistics,
|
||||
StatisticsMetaData,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
@ -38,7 +42,9 @@ import { LovelaceCard } from "../../types";
|
||||
import { EnergyDevicesDetailGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions } from "./common/energy-chart-options";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { clickIsTouch } from "../../../../components/chart/click_is_touch";
|
||||
|
||||
const UNIT = "kWh";
|
||||
|
||||
@ -72,6 +78,8 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
})
|
||||
private _hiddenStats: string[] = [];
|
||||
|
||||
private _untrackedIndex?: number;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
@ -149,17 +157,22 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
}
|
||||
|
||||
private _datasetHidden(ev) {
|
||||
this._hiddenStats = [
|
||||
...this._hiddenStats,
|
||||
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption,
|
||||
];
|
||||
const hiddenEntity =
|
||||
ev.detail.index === this._untrackedIndex
|
||||
? "untracked"
|
||||
: this._data!.prefs.device_consumption[ev.detail.index]
|
||||
.stat_consumption;
|
||||
this._hiddenStats = [...this._hiddenStats, hiddenEntity];
|
||||
}
|
||||
|
||||
private _datasetUnhidden(ev) {
|
||||
const hiddenEntity =
|
||||
ev.detail.index === this._untrackedIndex
|
||||
? "untracked"
|
||||
: this._data!.prefs.device_consumption[ev.detail.index]
|
||||
.stat_consumption;
|
||||
this._hiddenStats = this._hiddenStats.filter(
|
||||
(stat) =>
|
||||
stat !==
|
||||
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption
|
||||
(stat) => stat !== hiddenEntity
|
||||
);
|
||||
}
|
||||
|
||||
@ -197,6 +210,20 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
},
|
||||
},
|
||||
},
|
||||
onClick: (event, elements, chart) => {
|
||||
if (clickIsTouch(event)) return;
|
||||
|
||||
const index = elements[0]?.datasetIndex ?? -1;
|
||||
if (index < 0) return;
|
||||
|
||||
const statisticId =
|
||||
this._data?.prefs.device_consumption[index]?.stat_consumption;
|
||||
|
||||
if (!statisticId || isExternalStatistic(statisticId)) return;
|
||||
|
||||
fireEvent(this, "hass-more-info", { entityId: statisticId });
|
||||
chart?.canvas?.dispatchEvent(new Event("mouseout")); // to hide tooltip
|
||||
},
|
||||
};
|
||||
return options;
|
||||
}
|
||||
@ -240,6 +267,33 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
|
||||
datasetExtras.push(...processedDataExtras);
|
||||
|
||||
const { summedData, compareSummedData } = getSummedData(energyData);
|
||||
|
||||
const showUntracked =
|
||||
"from_grid" in summedData ||
|
||||
"solar" in summedData ||
|
||||
"from_battery" in summedData;
|
||||
|
||||
const {
|
||||
consumption: consumptionData,
|
||||
compareConsumption: consumptionCompareData,
|
||||
} = showUntracked
|
||||
? computeConsumptionData(summedData, compareSummedData)
|
||||
: { consumption: undefined, compareConsumption: undefined };
|
||||
|
||||
if (showUntracked) {
|
||||
this._untrackedIndex = datasets.length;
|
||||
const { dataset: untrackedData, datasetExtra: untrackedDataExtra } =
|
||||
this._processUntracked(
|
||||
computedStyle,
|
||||
processedData,
|
||||
consumptionData,
|
||||
false
|
||||
);
|
||||
datasets.push(untrackedData);
|
||||
datasetExtras.push(untrackedDataExtra);
|
||||
}
|
||||
|
||||
if (compareData) {
|
||||
// Add empty dataset to align the bars
|
||||
datasets.push({
|
||||
@ -272,6 +326,20 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
|
||||
datasets.push(...processedCompareData);
|
||||
datasetExtras.push(...processedCompareDataExtras);
|
||||
|
||||
if (showUntracked) {
|
||||
const {
|
||||
dataset: untrackedCompareData,
|
||||
datasetExtra: untrackedCompareDataExtra,
|
||||
} = this._processUntracked(
|
||||
computedStyle,
|
||||
processedCompareData,
|
||||
consumptionCompareData,
|
||||
true
|
||||
);
|
||||
datasets.push(untrackedCompareData);
|
||||
datasetExtras.push(untrackedCompareDataExtra);
|
||||
}
|
||||
}
|
||||
|
||||
this._start = energyData.start;
|
||||
@ -286,6 +354,57 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
this._chartDatasetExtra = datasetExtras;
|
||||
}
|
||||
|
||||
private _processUntracked(
|
||||
computedStyle: CSSStyleDeclaration,
|
||||
processedData,
|
||||
consumptionData,
|
||||
compare: boolean
|
||||
): { dataset; datasetExtra } {
|
||||
const totalDeviceConsumption: { [start: number]: number } = {};
|
||||
|
||||
processedData.forEach((device) => {
|
||||
device.data.forEach((datapoint) => {
|
||||
totalDeviceConsumption[datapoint.x] =
|
||||
(totalDeviceConsumption[datapoint.x] || 0) + datapoint.y;
|
||||
});
|
||||
});
|
||||
|
||||
const untrackedConsumption: { x: number; y: number }[] = [];
|
||||
Object.keys(consumptionData.total).forEach((time) => {
|
||||
untrackedConsumption.push({
|
||||
x: Number(time),
|
||||
y: consumptionData.total[time] - (totalDeviceConsumption[time] || 0),
|
||||
});
|
||||
});
|
||||
const dataset = {
|
||||
label: this.hass.localize("ui.panel.energy.charts.untracked_consumption"),
|
||||
hidden: this._hiddenStats.includes("untracked"),
|
||||
borderColor: getEnergyColor(
|
||||
computedStyle,
|
||||
this.hass.themes.darkMode,
|
||||
false,
|
||||
compare,
|
||||
"--state-unavailable-color"
|
||||
),
|
||||
backgroundColor: getEnergyColor(
|
||||
computedStyle,
|
||||
this.hass.themes.darkMode,
|
||||
true,
|
||||
compare,
|
||||
"--state-unavailable-color"
|
||||
),
|
||||
data: untrackedConsumption,
|
||||
order: 1 + this._untrackedIndex!,
|
||||
stack: "devices",
|
||||
pointStyle: compare ? false : "circle",
|
||||
xAxisID: compare ? "xAxisCompare" : undefined,
|
||||
};
|
||||
const datasetExtra = {
|
||||
show_legend: !compare,
|
||||
};
|
||||
return { dataset, datasetExtra };
|
||||
}
|
||||
|
||||
private _processDataSet(
|
||||
computedStyle: CSSStyleDeclaration,
|
||||
statistics: Statistics,
|
||||
|
@ -31,6 +31,7 @@ import { EnergyData, getEnergyDataCollection } from "../../../../data/energy";
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
getStatisticLabel,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import { FrontendLocaleData } from "../../../../data/translation";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
@ -38,6 +39,7 @@ import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergyDevicesGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { clickIsTouch } from "../../../../components/chart/click_is_touch";
|
||||
|
||||
@customElement("hui-energy-devices-graph-card")
|
||||
export class HuiEnergyDevicesGraphCard
|
||||
@ -158,15 +160,18 @@ export class HuiEnergyDevicesGraphCard
|
||||
// @ts-expect-error
|
||||
locale: numberFormatToLocale(this.hass.locale),
|
||||
onClick: (e: any) => {
|
||||
if (clickIsTouch(e)) return;
|
||||
const chart = e.chart;
|
||||
const canvasPosition = getRelativePosition(e, chart);
|
||||
|
||||
const index = Math.abs(
|
||||
chart.scales.y.getValueForPixel(canvasPosition.y)
|
||||
);
|
||||
// @ts-ignore
|
||||
const statisticId = this._chartData?.datasets[0]?.data[index]?.y;
|
||||
if (!statisticId || isExternalStatistic(statisticId)) return;
|
||||
fireEvent(this, "hass-more-info", {
|
||||
// @ts-ignore
|
||||
entityId: this._chartData?.datasets[0]?.data[index]?.y,
|
||||
entityId: statisticId,
|
||||
});
|
||||
chart.canvas.dispatchEvent(new Event("mouseout")); // to hide tooltip
|
||||
},
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { getEnergyColor } from "./common/color";
|
||||
@ -25,12 +26,14 @@ import {
|
||||
import {
|
||||
calculateStatisticSumGrowth,
|
||||
getStatisticLabel,
|
||||
isExternalStatistic,
|
||||
} from "../../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { LovelaceCard } from "../../types";
|
||||
import { EnergySourcesTableCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
const colorPropertyMap = {
|
||||
grid_return: "--energy-grid-return-color",
|
||||
@ -225,7 +228,13 @@ export class HuiEnergySourcesTableCard
|
||||
0;
|
||||
totalSolarCompare += compareEnergy;
|
||||
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
return html`<tr
|
||||
class="mdc-data-table__row ${classMap({
|
||||
clickable: !isExternalStatistic(source.stat_energy_from),
|
||||
})}"
|
||||
@click=${this._handleMoreInfo}
|
||||
.entity=${source.stat_energy_from}
|
||||
>
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
@ -330,7 +339,13 @@ export class HuiEnergySourcesTableCard
|
||||
0;
|
||||
totalBatteryCompare += energyFromCompare - energyToCompare;
|
||||
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
return html`<tr
|
||||
class="mdc-data-table__row ${classMap({
|
||||
clickable: !isExternalStatistic(source.stat_energy_from),
|
||||
})}"
|
||||
@click=${this._handleMoreInfo}
|
||||
.entity=${source.stat_energy_from}
|
||||
>
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
@ -381,7 +396,13 @@ export class HuiEnergySourcesTableCard
|
||||
? html`<td class="mdc-data-table__cell"></td>`
|
||||
: ""}
|
||||
</tr>
|
||||
<tr class="mdc-data-table__row">
|
||||
<tr
|
||||
class="mdc-data-table__row ${classMap({
|
||||
clickable: !isExternalStatistic(source.stat_energy_to),
|
||||
})}"
|
||||
@click=${this._handleMoreInfo}
|
||||
.entity=${source.stat_energy_to}
|
||||
>
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
@ -508,7 +529,13 @@ export class HuiEnergySourcesTableCard
|
||||
totalGridCostCompare += costCompare;
|
||||
}
|
||||
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
return html`<tr
|
||||
class="mdc-data-table__row ${classMap({
|
||||
clickable: !isExternalStatistic(flow.stat_energy_from),
|
||||
})}"
|
||||
@click=${this._handleMoreInfo}
|
||||
.entity=${flow.stat_energy_from}
|
||||
>
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
@ -619,7 +646,13 @@ export class HuiEnergySourcesTableCard
|
||||
totalGridCostCompare += costCompare;
|
||||
}
|
||||
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
return html`<tr
|
||||
class="mdc-data-table__row ${classMap({
|
||||
clickable: !isExternalStatistic(flow.stat_energy_to),
|
||||
})}"
|
||||
@click=${this._handleMoreInfo}
|
||||
.entity=${flow.stat_energy_to}
|
||||
>
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
@ -784,7 +817,13 @@ export class HuiEnergySourcesTableCard
|
||||
totalGasCostCompare += costCompare;
|
||||
}
|
||||
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
return html`<tr
|
||||
class="mdc-data-table__row ${classMap({
|
||||
clickable: !isExternalStatistic(source.stat_energy_from),
|
||||
})}"
|
||||
@click=${this._handleMoreInfo}
|
||||
.entity=${source.stat_energy_from}
|
||||
>
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
@ -942,7 +981,13 @@ export class HuiEnergySourcesTableCard
|
||||
totalWaterCostCompare += costCompare;
|
||||
}
|
||||
|
||||
return html`<tr class="mdc-data-table__row">
|
||||
return html`<tr
|
||||
class="mdc-data-table__row ${classMap({
|
||||
clickable: !isExternalStatistic(source.stat_energy_from),
|
||||
})}"
|
||||
@click=${this._handleMoreInfo}
|
||||
.entity=${source.stat_energy_from}
|
||||
>
|
||||
<td class="mdc-data-table__cell cell-bullet">
|
||||
<div
|
||||
class="bullet"
|
||||
@ -1111,6 +1156,13 @@ export class HuiEnergySourcesTableCard
|
||||
</ha-card>`;
|
||||
}
|
||||
|
||||
private _handleMoreInfo(ev): void {
|
||||
const entityId = ev.currentTarget?.entity;
|
||||
if (entityId && !isExternalStatistic(entityId)) {
|
||||
fireEvent(this, "hass-more-info", { entityId });
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
${unsafeCSS(dataTableStyles)}
|
||||
@ -1127,6 +1179,9 @@ export class HuiEnergySourcesTableCard
|
||||
.mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.04);
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.total {
|
||||
--mdc-typography-body2-font-weight: 500;
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
return {
|
||||
grid_columns: 4,
|
||||
grid_columns: "full",
|
||||
grid_rows: 4,
|
||||
grid_min_rows: 2,
|
||||
};
|
||||
|
@ -426,7 +426,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public getLayoutOptions(): LovelaceLayoutOptions {
|
||||
return {
|
||||
grid_columns: 4,
|
||||
grid_columns: "full",
|
||||
grid_rows: 4,
|
||||
grid_min_columns: 2,
|
||||
grid_min_rows: 2,
|
||||
|
36
src/panels/lovelace/common/compute-card-grid-size.ts
Normal file
36
src/panels/lovelace/common/compute-card-grid-size.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { conditionalClamp } from "../../../common/number/clamp";
|
||||
import { LovelaceLayoutOptions } from "../types";
|
||||
|
||||
export const DEFAULT_GRID_SIZE = {
|
||||
columns: 4,
|
||||
rows: "auto",
|
||||
} as CardGridSize;
|
||||
|
||||
export type CardGridSize = {
|
||||
rows: number | "auto";
|
||||
columns: number | "full";
|
||||
};
|
||||
|
||||
export const computeCardGridSize = (
|
||||
options: LovelaceLayoutOptions
|
||||
): CardGridSize => {
|
||||
const rows = options.grid_rows ?? DEFAULT_GRID_SIZE.rows;
|
||||
const columns = options.grid_columns ?? DEFAULT_GRID_SIZE.columns;
|
||||
const minRows = options.grid_min_rows;
|
||||
const maxRows = options.grid_max_rows;
|
||||
const minColumns = options.grid_min_columns;
|
||||
const maxColumns = options.grid_max_columns;
|
||||
|
||||
const clampedRows =
|
||||
typeof rows === "string" ? rows : conditionalClamp(rows, minRows, maxRows);
|
||||
|
||||
const clampedColumns =
|
||||
typeof columns === "string"
|
||||
? columns
|
||||
: conditionalClamp(columns, minColumns, maxColumns);
|
||||
|
||||
return {
|
||||
rows: clampedRows,
|
||||
columns: clampedColumns,
|
||||
};
|
||||
};
|
@ -44,6 +44,7 @@ const HIDE_DOMAIN = new Set([
|
||||
"persistent_notification",
|
||||
"script",
|
||||
"sun",
|
||||
"tag",
|
||||
"todo",
|
||||
"zone",
|
||||
...ASSIST_ENTITIES,
|
||||
|
@ -226,8 +226,15 @@ export class HuiActionEditor extends LitElement {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
let action = this.config?.action;
|
||||
|
||||
if (action === "call-service") {
|
||||
action = "perform-action";
|
||||
}
|
||||
|
||||
const value = ev.target.value;
|
||||
if (this.config?.action === value) {
|
||||
|
||||
if (action === value) {
|
||||
return;
|
||||
}
|
||||
if (value === "default") {
|
||||
@ -292,6 +299,7 @@ export class HuiActionEditor extends LitElement {
|
||||
ev.stopPropagation();
|
||||
const value = {
|
||||
...this.config!,
|
||||
action: "perform-action",
|
||||
perform_action: ev.detail.value.action || "",
|
||||
data: ev.detail.value.data,
|
||||
target: ev.detail.value.target || {},
|
||||
|
@ -1,16 +1,15 @@
|
||||
import { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import "../cards/hui-button-card";
|
||||
import "../cards/hui-calendar-card";
|
||||
import "../cards/hui-entities-card";
|
||||
import "../cards/hui-entity-button-card";
|
||||
import "../cards/hui-entity-card";
|
||||
import "../cards/hui-entities-card";
|
||||
import "../cards/hui-button-card";
|
||||
import "../cards/hui-entity-button-card";
|
||||
import "../cards/hui-glance-card";
|
||||
import "../cards/hui-grid-card";
|
||||
import "../cards/hui-light-card";
|
||||
import "../cards/hui-sensor-card";
|
||||
import "../cards/hui-thermostat-card";
|
||||
import "../cards/hui-tile-card";
|
||||
import "../cards/hui-weather-forecast-card";
|
||||
import "../cards/hui-tile-card";
|
||||
import {
|
||||
createLovelaceElement,
|
||||
getLovelaceElementClass,
|
||||
|
@ -8,6 +8,7 @@ import type { LovelaceCardEditor, LovelaceConfigForm } from "../../types";
|
||||
import { HuiElementEditor } from "../hui-element-editor";
|
||||
import "./hui-card-layout-editor";
|
||||
import "./hui-card-visibility-editor";
|
||||
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
|
||||
const tabs = ["config", "visibility", "layout"] as const;
|
||||
|
||||
@ -16,8 +17,7 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||
@property({ type: Boolean, attribute: "show-visibility-tab" })
|
||||
public showVisibilityTab = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-layout-tab" })
|
||||
public showLayoutTab = false;
|
||||
@property({ attribute: false }) public sectionConfig?: LovelaceSectionConfig;
|
||||
|
||||
@state() private _currTab: (typeof tabs)[number] = tabs[0];
|
||||
|
||||
@ -48,10 +48,18 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||
this.value = ev.detail.value;
|
||||
}
|
||||
|
||||
get _showLayoutTab(): boolean {
|
||||
return (
|
||||
!!this.sectionConfig &&
|
||||
(this.sectionConfig.type === undefined ||
|
||||
this.sectionConfig.type === "grid")
|
||||
);
|
||||
}
|
||||
|
||||
protected renderConfigElement(): TemplateResult {
|
||||
const displayedTabs: string[] = ["config"];
|
||||
if (this.showVisibilityTab) displayedTabs.push("visibility");
|
||||
if (this.showLayoutTab) displayedTabs.push("layout");
|
||||
if (this._showLayoutTab) displayedTabs.push("layout");
|
||||
|
||||
if (displayedTabs.length === 1) return super.renderConfigElement();
|
||||
|
||||
@ -75,6 +83,7 @@ export class HuiCardElementEditor extends HuiElementEditor<LovelaceCardConfig> {
|
||||
<hui-card-layout-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this.value}
|
||||
.sectionConfig=${this.sectionConfig!}
|
||||
@value-changed=${this._configChanged}
|
||||
>
|
||||
</hui-card-layout-editor>
|
||||
|
@ -1,7 +1,8 @@
|
||||
import type { ActionDetail } from "@material/mwc-list";
|
||||
import { mdiCheck, mdiDotsVertical } from "@mdi/js";
|
||||
import { LitElement, PropertyValues, css, html, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { preventDefault } from "../../../../common/dom/prevent_default";
|
||||
@ -18,10 +19,14 @@ import "../../../../components/ha-switch";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import { LovelaceSectionConfig } from "../../../../data/lovelace/config/section";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import { HuiCard } from "../../cards/hui-card";
|
||||
import { computeSizeOnGrid } from "../../sections/hui-grid-section";
|
||||
import {
|
||||
CardGridSize,
|
||||
computeCardGridSize,
|
||||
} from "../../common/compute-card-grid-size";
|
||||
import { LovelaceLayoutOptions } from "../../types";
|
||||
|
||||
@customElement("hui-card-layout-editor")
|
||||
@ -30,6 +35,8 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public config!: LovelaceCardConfig;
|
||||
|
||||
@property({ attribute: false }) public sectionConfig!: LovelaceSectionConfig;
|
||||
|
||||
@state() _defaultLayoutOptions?: LovelaceLayoutOptions;
|
||||
|
||||
@state() public _yamlMode = false;
|
||||
@ -50,7 +57,7 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _gridSizeValue = memoizeOne(computeSizeOnGrid);
|
||||
private _computeCardGridSize = memoizeOne(computeCardGridSize);
|
||||
|
||||
private _isDefault = memoizeOne(
|
||||
(options?: LovelaceLayoutOptions) =>
|
||||
@ -63,7 +70,9 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
this._defaultLayoutOptions
|
||||
);
|
||||
|
||||
const sizeValue = this._gridSizeValue(options);
|
||||
const value = this._computeCardGridSize(options);
|
||||
|
||||
const totalColumns = (this.sectionConfig.column_span ?? 1) * 4;
|
||||
|
||||
return html`
|
||||
<div class="header">
|
||||
@ -127,8 +136,12 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<ha-grid-size-picker
|
||||
style=${styleMap({
|
||||
"max-width": `${totalColumns * 45 + 50}px`,
|
||||
})}
|
||||
.columns=${totalColumns}
|
||||
.hass=${this.hass}
|
||||
.value=${sizeValue}
|
||||
.value=${value}
|
||||
.isDefault=${this._isDefault(this.config.layout_options)}
|
||||
@value-changed=${this._gridSizeChanged}
|
||||
.rowMin=${options.grid_min_rows}
|
||||
@ -136,6 +149,24 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
.columnMin=${options.grid_min_columns}
|
||||
.columnMax=${options.grid_max_columns}
|
||||
></ha-grid-size-picker>
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="full-width">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.layout.full_width"
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="full-width">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.layout.full_width_helper"
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._fullWidthChanged}
|
||||
.checked=${value.columns === "full"}
|
||||
name="full-width"
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
@ -195,7 +226,7 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
|
||||
private _gridSizeChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.detail.value;
|
||||
const value = ev.detail.value as CardGridSize;
|
||||
|
||||
const newConfig: LovelaceCardConfig = {
|
||||
...this.config,
|
||||
@ -229,6 +260,21 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
|
||||
private _fullWidthChanged(ev): void {
|
||||
ev.stopPropagation();
|
||||
const value = ev.target.checked;
|
||||
const newConfig: LovelaceCardConfig = {
|
||||
...this.config,
|
||||
layout_options: {
|
||||
...this.config.layout_options,
|
||||
grid_columns: value
|
||||
? "full"
|
||||
: (this._defaultLayoutOptions?.grid_min_columns ?? 1),
|
||||
},
|
||||
};
|
||||
fireEvent(this, "value-changed", { value: newConfig });
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyle,
|
||||
css`
|
||||
@ -255,7 +301,6 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
}
|
||||
ha-grid-size-picker {
|
||||
display: block;
|
||||
max-width: 250px;
|
||||
margin: 16px auto;
|
||||
}
|
||||
ha-yaml-editor {
|
||||
|
@ -236,8 +236,10 @@ export class HuiDialogEditCard
|
||||
<div class="content">
|
||||
<div class="element-editor">
|
||||
<hui-card-element-editor
|
||||
.showLayoutTab=${this._shouldShowLayoutTab()}
|
||||
.showVisibilityTab=${this._cardConfig?.type !== "conditional"}
|
||||
.sectionConfig=${this._isInSection
|
||||
? this._containerConfig
|
||||
: undefined}
|
||||
.hass=${this.hass}
|
||||
.lovelace=${this._params.lovelaceConfig}
|
||||
.value=${this._cardConfig}
|
||||
@ -353,18 +355,6 @@ export class HuiDialogEditCard
|
||||
return this._params!.path.length === 2;
|
||||
}
|
||||
|
||||
private _shouldShowLayoutTab(): boolean {
|
||||
/**
|
||||
* Only show layout tab for cards in a grid section
|
||||
* In the future, every section and view should be able to bring their own editor for layout.
|
||||
* For now, we limit it to grid sections as it's the only section type
|
||||
* */
|
||||
return (
|
||||
this._isInSection &&
|
||||
(!this._containerConfig.type || this._containerConfig.type === "grid")
|
||||
);
|
||||
}
|
||||
|
||||
private _cardConfigInSection = memoizeOne(
|
||||
(cardConfig?: LovelaceCardConfig) => {
|
||||
const { cards, title, ...containerConfig } = this
|
||||
|
@ -102,7 +102,7 @@ export class HaCardConditionState extends LitElement {
|
||||
const data: StateConditionData = {
|
||||
...content,
|
||||
entity: this.condition.entity,
|
||||
invert: this.condition.state_not ? "true" : "false",
|
||||
invert: this.condition.state_not !== undefined ? "true" : "false",
|
||||
state: this.condition.state_not ?? this.condition.state,
|
||||
};
|
||||
|
||||
|
@ -22,8 +22,9 @@ import type {
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
DEFAULT_DISPLAY_TYPE,
|
||||
DEFAULT_CONFIG,
|
||||
DISPLAY_TYPES,
|
||||
migrateLegacyEntityBadgeConfig,
|
||||
} from "../../badges/hui-entity-badge";
|
||||
import { EntityBadgeConfig } from "../../badges/types";
|
||||
import type { LovelaceBadgeEditor } from "../../types";
|
||||
@ -42,10 +43,12 @@ const badgeConfigStruct = assign(
|
||||
icon: optional(string()),
|
||||
state_content: optional(union([string(), array(string())])),
|
||||
color: optional(string()),
|
||||
show_name: optional(boolean()),
|
||||
show_state: optional(boolean()),
|
||||
show_icon: optional(boolean()),
|
||||
show_entity_picture: optional(boolean()),
|
||||
tap_action: optional(actionConfigStruct),
|
||||
show_name: optional(boolean()),
|
||||
image: optional(string()),
|
||||
image: optional(string()), // For old badge config support
|
||||
})
|
||||
);
|
||||
|
||||
@ -60,7 +63,10 @@ export class HuiEntityBadgeEditor
|
||||
|
||||
public setConfig(config: EntityBadgeConfig): void {
|
||||
assert(config, badgeConfigStruct);
|
||||
this._config = config;
|
||||
this._config = {
|
||||
...DEFAULT_CONFIG,
|
||||
...migrateLegacyEntityBadgeConfig(config),
|
||||
};
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
@ -68,25 +74,11 @@ export class HuiEntityBadgeEditor
|
||||
[
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "",
|
||||
name: "appearance",
|
||||
type: "expandable",
|
||||
flatten: true,
|
||||
iconPath: mdiPalette,
|
||||
title: localize(`ui.panel.lovelace.editor.badge.entity.appearance`),
|
||||
schema: [
|
||||
{
|
||||
name: "display_type",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: DISPLAY_TYPES.map((type) => ({
|
||||
value: type,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.display_type_options.${type}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
type: "grid",
|
||||
@ -97,6 +89,12 @@ export class HuiEntityBadgeEditor
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: { default_color: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "icon",
|
||||
selector: {
|
||||
@ -104,12 +102,6 @@ export class HuiEntityBadgeEditor
|
||||
},
|
||||
context: { icon_entity: "entity" },
|
||||
},
|
||||
{
|
||||
name: "color",
|
||||
selector: {
|
||||
ui_color: { default_color: true },
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "show_entity_picture",
|
||||
selector: {
|
||||
@ -118,7 +110,35 @@ export class HuiEntityBadgeEditor
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "displayed_elements",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
multiple: true,
|
||||
options: [
|
||||
{
|
||||
value: "name",
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.name`
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "state",
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.state`
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "icon",
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.displayed_elements_options.icon`
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "state_content",
|
||||
selector: {
|
||||
@ -133,9 +153,9 @@ export class HuiEntityBadgeEditor
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
title: localize(`ui.panel.lovelace.editor.badge.entity.interactions`),
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
@ -151,6 +171,20 @@ export class HuiEntityBadgeEditor
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
_displayedElements = memoizeOne((config: EntityBadgeConfig) => {
|
||||
const elements: string[] = [];
|
||||
if (config.show_name) {
|
||||
elements.push("name");
|
||||
}
|
||||
if (config.show_state) {
|
||||
elements.push("state");
|
||||
}
|
||||
if (config.show_icon) {
|
||||
elements.push("icon");
|
||||
}
|
||||
return elements;
|
||||
});
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
@ -158,11 +192,10 @@ export class HuiEntityBadgeEditor
|
||||
|
||||
const schema = this._schema(this.hass!.localize);
|
||||
|
||||
const data = { ...this._config };
|
||||
|
||||
if (!data.display_type) {
|
||||
data.display_type = DEFAULT_DISPLAY_TYPE;
|
||||
}
|
||||
const data = {
|
||||
...this._config,
|
||||
displayed_elements: this._displayedElements(this._config),
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
@ -181,18 +214,17 @@ export class HuiEntityBadgeEditor
|
||||
return;
|
||||
}
|
||||
|
||||
const newConfig = ev.detail.value as EntityBadgeConfig;
|
||||
|
||||
const config: EntityBadgeConfig = {
|
||||
...newConfig,
|
||||
};
|
||||
const config = { ...ev.detail.value } as EntityBadgeConfig;
|
||||
|
||||
if (!config.state_content) {
|
||||
delete config.state_content;
|
||||
}
|
||||
|
||||
if (config.display_type === "standard") {
|
||||
delete config.display_type;
|
||||
if (config.displayed_elements) {
|
||||
config.show_name = config.displayed_elements.includes("name");
|
||||
config.show_state = config.displayed_elements.includes("state");
|
||||
config.show_icon = config.displayed_elements.includes("icon");
|
||||
delete config.displayed_elements;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
@ -204,8 +236,10 @@ export class HuiEntityBadgeEditor
|
||||
switch (schema.name) {
|
||||
case "color":
|
||||
case "state_content":
|
||||
case "display_type":
|
||||
case "show_entity_picture":
|
||||
case "displayed_elements":
|
||||
case "appearance":
|
||||
case "interactions":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.badge.entity.${schema.name}`
|
||||
);
|
||||
|
@ -30,11 +30,11 @@ export class HuiStateLabelBadgeEditor extends HuiEntityBadgeEditor {
|
||||
const entityBadgeConfig: EntityBadgeConfig = {
|
||||
type: "entity",
|
||||
entity: config.entity,
|
||||
display_type: config.show_name === false ? "standard" : "complete",
|
||||
show_name: config.show_name ?? true,
|
||||
};
|
||||
|
||||
// @ts-ignore
|
||||
this._config = entityBadgeConfig;
|
||||
super.setConfig(entityBadgeConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,6 @@ import {
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
@ -69,18 +68,14 @@ export class HuiTileCardEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
entityId: string | undefined,
|
||||
hideState: boolean
|
||||
) =>
|
||||
(entityId: string | undefined, hideState: boolean) =>
|
||||
[
|
||||
{ name: "entity", selector: { entity: {} } },
|
||||
{
|
||||
name: "",
|
||||
name: "appearance",
|
||||
flatten: true,
|
||||
type: "expandable",
|
||||
iconPath: mdiPalette,
|
||||
title: localize(`ui.panel.lovelace.editor.card.tile.appearance`),
|
||||
schema: [
|
||||
{
|
||||
name: "",
|
||||
@ -136,9 +131,9 @@ export class HuiTileCardEditor
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
name: "interactions",
|
||||
type: "expandable",
|
||||
title: localize(`ui.panel.lovelace.editor.card.tile.interactions`),
|
||||
flatten: true,
|
||||
iconPath: mdiGestureTap,
|
||||
schema: [
|
||||
{
|
||||
@ -178,7 +173,6 @@ export class HuiTileCardEditor
|
||||
: undefined;
|
||||
|
||||
const schema = this._schema(
|
||||
this.hass!.localize,
|
||||
this._config.entity,
|
||||
this._config.hide_state ?? false
|
||||
);
|
||||
@ -306,6 +300,8 @@ export class HuiTileCardEditor
|
||||
case "vertical":
|
||||
case "hide_state":
|
||||
case "state_content":
|
||||
case "appearance":
|
||||
case "interactions":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.${schema.name}`
|
||||
);
|
||||
|
@ -35,6 +35,7 @@ import "./hui-section-visibility-editor";
|
||||
import type { EditSectionDialogParams } from "./show-edit-section-dialog";
|
||||
import "@material/mwc-tab-bar/mwc-tab-bar";
|
||||
import "@material/mwc-tab/mwc-tab";
|
||||
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
|
||||
const TABS = ["tab-settings", "tab-visibility"] as const;
|
||||
|
||||
@ -49,6 +50,8 @@ export class HuiDialogEditSection
|
||||
|
||||
@state() private _config?: LovelaceSectionRawConfig;
|
||||
|
||||
@state() private _viewConfig?: LovelaceViewConfig;
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@state() private _currTab: (typeof TABS)[number] = TABS[0];
|
||||
@ -57,10 +60,10 @@ export class HuiDialogEditSection
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (this._yamlMode && changedProperties.has("_yamlMode")) {
|
||||
const viewConfig = {
|
||||
const sectionConfig = {
|
||||
...this._config,
|
||||
};
|
||||
this._editor?.setValue(viewConfig);
|
||||
this._editor?.setValue(sectionConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +74,9 @@ export class HuiDialogEditSection
|
||||
this._params.viewIndex,
|
||||
this._params.sectionIndex,
|
||||
]);
|
||||
this._viewConfig = findLovelaceContainer(this._params.lovelaceConfig, [
|
||||
this._params.viewIndex,
|
||||
]);
|
||||
}
|
||||
|
||||
public closeDialog() {
|
||||
@ -107,6 +113,7 @@ export class HuiDialogEditSection
|
||||
<hui-section-settings-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
.viewConfig=${this._viewConfig}
|
||||
@value-changed=${this._configChanged}
|
||||
>
|
||||
</hui-section-settings-editor>
|
||||
|
@ -1,22 +1,19 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "title",
|
||||
selector: { text: {} },
|
||||
},
|
||||
] as const satisfies HaFormSchema[];
|
||||
import { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
type SettingsData = {
|
||||
title: string;
|
||||
column_span?: number;
|
||||
};
|
||||
|
||||
@customElement("hui-section-settings-editor")
|
||||
@ -25,16 +22,46 @@ export class HuiDialogEditSection extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
|
||||
|
||||
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, maxColumns: number) =>
|
||||
[
|
||||
{
|
||||
name: "title",
|
||||
selector: { text: {} },
|
||||
},
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
unit_of_measurement: localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.column_span_unit`
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
render() {
|
||||
const data: SettingsData = {
|
||||
title: this.config.title || "",
|
||||
column_span: this.config.column_span || 1,
|
||||
};
|
||||
|
||||
const schema = this._schema(
|
||||
this.hass.localize,
|
||||
this.viewConfig.max_columns || 4
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
@ -42,12 +69,16 @@ export class HuiDialogEditSection extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}`
|
||||
);
|
||||
|
||||
private _computeHelper = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
private _computeHelper = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}_helper`
|
||||
) || "";
|
||||
@ -59,6 +90,7 @@ export class HuiDialogEditSection extends LitElement {
|
||||
const newConfig: LovelaceSectionRawConfig = {
|
||||
...this.config,
|
||||
title: newData.title,
|
||||
column_span: newData.column_span,
|
||||
};
|
||||
|
||||
if (!newConfig.title) {
|
||||
|
@ -41,6 +41,8 @@ const actionConfigStructService = object({
|
||||
entity_id: optional(union([string(), array(string())])),
|
||||
device_id: optional(union([string(), array(string())])),
|
||||
area_id: optional(union([string(), array(string())])),
|
||||
floor_id: optional(union([string(), array(string())])),
|
||||
label_id: optional(union([string(), array(string())])),
|
||||
})
|
||||
),
|
||||
confirmation: optional(actionConfigStructConfirmation),
|
||||
|
@ -231,14 +231,15 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
if (!value) {
|
||||
showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.confirm_remove_config_title"
|
||||
"ui.panel.lovelace.editor.raw_editor.confirm_delete_config_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.confirm_remove_config_text"
|
||||
"ui.panel.lovelace.editor.raw_editor.confirm_delete_config_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.remove"),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirm: () => this._removeConfig(),
|
||||
destructive: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import "../components/hui-card-edit-mode";
|
||||
import { moveCard } from "../editor/config-util";
|
||||
import type { Lovelace, LovelaceLayoutOptions } from "../types";
|
||||
import { conditionalClamp } from "../../../common/number/clamp";
|
||||
import type { Lovelace } from "../types";
|
||||
import { computeCardGridSize } from "../common/compute-card-grid-size";
|
||||
|
||||
const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
delay: 100,
|
||||
@ -24,43 +24,6 @@ const CARD_SORTABLE_OPTIONS: HaSortableOptions = {
|
||||
invertedSwapThreshold: 0.7,
|
||||
} as HaSortableOptions;
|
||||
|
||||
export const DEFAULT_GRID_OPTIONS = {
|
||||
grid_columns: 4,
|
||||
grid_rows: "auto",
|
||||
} as const satisfies LovelaceLayoutOptions;
|
||||
|
||||
type GridSizeValue = {
|
||||
rows?: number | "auto";
|
||||
columns?: number;
|
||||
};
|
||||
|
||||
export const computeSizeOnGrid = (
|
||||
options: LovelaceLayoutOptions
|
||||
): GridSizeValue => {
|
||||
const rows =
|
||||
typeof options.grid_rows === "number"
|
||||
? conditionalClamp(
|
||||
options.grid_rows,
|
||||
options.grid_min_rows,
|
||||
options.grid_max_rows
|
||||
)
|
||||
: DEFAULT_GRID_OPTIONS.grid_rows;
|
||||
|
||||
const columns =
|
||||
typeof options.grid_columns === "number"
|
||||
? conditionalClamp(
|
||||
options.grid_columns,
|
||||
options.grid_min_columns,
|
||||
options.grid_max_columns
|
||||
)
|
||||
: DEFAULT_GRID_OPTIONS.grid_columns;
|
||||
|
||||
return {
|
||||
rows,
|
||||
columns,
|
||||
};
|
||||
};
|
||||
|
||||
export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@ -134,16 +97,18 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
card.layout = "grid";
|
||||
const layoutOptions = card.getLayoutOptions();
|
||||
|
||||
const { rows, columns } = computeSizeOnGrid(layoutOptions);
|
||||
const { rows, columns } = computeCardGridSize(layoutOptions);
|
||||
|
||||
return html`
|
||||
<div
|
||||
style=${styleMap({
|
||||
"--column-size": columns,
|
||||
"--row-size": rows,
|
||||
"--column-size":
|
||||
typeof columns === "number" ? columns : undefined,
|
||||
"--row-size": typeof rows === "number" ? rows : undefined,
|
||||
})}
|
||||
class="card ${classMap({
|
||||
"fit-rows": typeof layoutOptions?.grid_rows === "number",
|
||||
"full-width": columns === "full",
|
||||
})}"
|
||||
>
|
||||
${editMode
|
||||
@ -211,7 +176,7 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
--column-count: 4;
|
||||
--base-column-count: 4;
|
||||
--row-gap: var(--ha-section-grid-row-gap, 8px);
|
||||
--column-gap: var(--ha-section-grid-column-gap, 8px);
|
||||
--row-height: var(--ha-section-grid-row-height, 56px);
|
||||
@ -220,8 +185,14 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
gap: var(--row-gap);
|
||||
}
|
||||
.container {
|
||||
--grid-column-count: calc(
|
||||
var(--base-column-count) * var(--column-span, 1)
|
||||
);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--column-count), minmax(0, 1fr));
|
||||
grid-template-columns: repeat(
|
||||
var(--grid-column-count),
|
||||
minmax(0, 1fr)
|
||||
);
|
||||
grid-auto-rows: minmax(var(--row-height), auto);
|
||||
row-gap: var(--row-gap);
|
||||
column-gap: var(--column-gap);
|
||||
@ -262,8 +233,8 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
.card {
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
position: relative;
|
||||
grid-row: span var(--row-size);
|
||||
grid-column: span var(--column-size);
|
||||
grid-row: span var(--row-size, 1);
|
||||
grid-column: span min(var(--column-size, 1), var(--grid-column-count));
|
||||
}
|
||||
|
||||
.card.fit-rows {
|
||||
@ -274,6 +245,10 @@ export class GridSection extends LitElement implements LovelaceSectionElement {
|
||||
);
|
||||
}
|
||||
|
||||
.card.full-width {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.card:has(> *) {
|
||||
display: block;
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ export interface LovelaceBadge extends HTMLElement {
|
||||
}
|
||||
|
||||
export type LovelaceLayoutOptions = {
|
||||
grid_columns?: number;
|
||||
grid_columns?: number | "full";
|
||||
grid_rows?: number | "auto";
|
||||
grid_max_columns?: number;
|
||||
grid_min_columns?: number;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiArrowAll, mdiDelete, mdiPencil, mdiViewGridPlus } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
@ -26,6 +27,10 @@ import { showEditSectionDialog } from "../editor/section-editor/show-edit-sectio
|
||||
import { HuiSection } from "../sections/hui-section";
|
||||
import type { Lovelace } from "../types";
|
||||
|
||||
export const DEFAULT_MAX_COLUMNS = 4;
|
||||
|
||||
const parsePx = (value: string) => parseInt(value.replace("px", ""));
|
||||
|
||||
@customElement("hui-sections-view")
|
||||
export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -46,6 +51,30 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@state() _dragging = false;
|
||||
|
||||
private _columnsController = new ResizeController(this, {
|
||||
callback: (entries) => {
|
||||
const totalWidth = entries[0]?.contentRect.width;
|
||||
|
||||
const style = getComputedStyle(this);
|
||||
const container = this.shadowRoot!.querySelector(".container")!;
|
||||
const containerStyle = getComputedStyle(container);
|
||||
|
||||
const paddingLeft = parsePx(containerStyle.paddingLeft);
|
||||
const paddingRight = parsePx(containerStyle.paddingRight);
|
||||
const padding = paddingLeft + paddingRight;
|
||||
const minColumnWidth = parsePx(
|
||||
style.getPropertyValue("--column-min-width")
|
||||
);
|
||||
const columnGap = parsePx(containerStyle.columnGap);
|
||||
|
||||
const columns = Math.floor(
|
||||
(totalWidth - padding + columnGap) / (minColumnWidth + columnGap)
|
||||
);
|
||||
const maxColumns = this._config?.max_columns ?? DEFAULT_MAX_COLUMNS;
|
||||
return Math.max(Math.min(maxColumns, columns), 1);
|
||||
},
|
||||
});
|
||||
|
||||
public setConfig(config: LovelaceViewConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
@ -95,10 +124,11 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
if (!this.lovelace) return nothing;
|
||||
|
||||
const sections = this.sections;
|
||||
const totalCount = this._sectionCount + (this.lovelace?.editMode ? 1 : 0);
|
||||
const totalSectionCount =
|
||||
this._sectionCount + (this.lovelace?.editMode ? 1 : 0);
|
||||
const editMode = this.lovelace.editMode;
|
||||
|
||||
const maxColumnsCount = this._config?.max_columns;
|
||||
const maxColumnCount = this._columnsController.value ?? 1;
|
||||
|
||||
return html`
|
||||
<hui-view-badges
|
||||
@ -118,17 +148,29 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
<div
|
||||
class="container"
|
||||
style=${styleMap({
|
||||
"--max-columns-count": maxColumnsCount,
|
||||
"--total-count": totalCount,
|
||||
"--total-section-count": totalSectionCount,
|
||||
"--max-column-count": maxColumnCount,
|
||||
})}
|
||||
>
|
||||
${repeat(
|
||||
sections,
|
||||
(section) => this._getSectionKey(section),
|
||||
(section, idx) => {
|
||||
const sectionConfig = this._config?.sections?.[idx];
|
||||
const columnSpan = Math.min(
|
||||
sectionConfig?.column_span || 1,
|
||||
maxColumnCount
|
||||
);
|
||||
|
||||
(section as any).itemPath = [idx];
|
||||
|
||||
return html`
|
||||
<div class="section">
|
||||
<div
|
||||
class="section"
|
||||
style=${styleMap({
|
||||
"--column-span": columnSpan,
|
||||
})}
|
||||
>
|
||||
${editMode
|
||||
? html`
|
||||
<div class="section-overlay">
|
||||
@ -252,19 +294,19 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
--row-height: var(--ha-view-sections-row-height, 56px);
|
||||
--row-gap: var(--ha-view-sections-row-gap, 8px);
|
||||
--column-gap: var(--ha-view-sections-column-gap, 32px);
|
||||
--column-min-width: var(--ha-view-sections-column-min-width, 320px);
|
||||
--column-max-width: var(--ha-view-sections-column-max-width, 500px);
|
||||
--column-min-width: var(--ha-view-sections-column-min-width, 320px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
position: relative;
|
||||
max-width: var(--column-max-width);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section {
|
||||
border-radius: var(--ha-card-border-radius, 12px);
|
||||
grid-column: span var(--column-span);
|
||||
}
|
||||
|
||||
.section:not(:has(> *:not([hidden]))) {
|
||||
@ -272,29 +314,22 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
||||
}
|
||||
|
||||
.container {
|
||||
--max-count: min(var(--total-count), var(--max-columns-count, 4));
|
||||
--max-width: min(
|
||||
calc(
|
||||
(var(--max-count) + 1) * var(--column-min-width) +
|
||||
(var(--max-count) + 2) * var(--column-gap) - 1px
|
||||
),
|
||||
calc(
|
||||
var(--max-count) * var(--column-max-width) + (var(--max-count) + 1) *
|
||||
var(--column-gap)
|
||||
)
|
||||
--column-count: min(
|
||||
var(--max-column-count),
|
||||
var(--total-section-count)
|
||||
);
|
||||
display: grid;
|
||||
align-items: start;
|
||||
justify-items: center;
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(min(var(--column-min-width), 100%), 1fr)
|
||||
);
|
||||
justify-content: center;
|
||||
grid-template-columns: repeat(var(--column-count), 1fr);
|
||||
gap: var(--row-gap) var(--column-gap);
|
||||
padding: var(--row-gap) var(--column-gap);
|
||||
box-sizing: border-box;
|
||||
max-width: var(--max-width);
|
||||
box-sizing: content-box;
|
||||
margin: 0 auto;
|
||||
max-width: calc(
|
||||
var(--column-count) * var(--column-max-width) +
|
||||
(var(--column-count) - 1) * var(--column-gap)
|
||||
);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
|
@ -12,6 +12,8 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LovelaceViewElement } from "../../../data/lovelace";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { HuiBadge } from "../badges/hui-badge";
|
||||
import "../badges/hui-view-badges";
|
||||
import { HuiCard } from "../cards/hui-card";
|
||||
import { HuiCardOptions } from "../components/hui-card-options";
|
||||
import { replaceCard } from "../editor/config-util";
|
||||
@ -28,6 +30,8 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
@property({ attribute: false }) public cards: HuiCard[] = [];
|
||||
|
||||
@property({ attribute: false }) public badges: HuiBadge[] = [];
|
||||
|
||||
@state() private _config?: LovelaceViewConfig;
|
||||
|
||||
private _mqlListenerRef?: () => void;
|
||||
@ -85,6 +89,12 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hui-view-badges
|
||||
.hass=${this.hass}
|
||||
.badges=${this.badges}
|
||||
.lovelace=${this.lovelace}
|
||||
.viewIndex=${this.index}
|
||||
></hui-view-badges>
|
||||
<div
|
||||
class="container ${this.lovelace?.editMode ? "edit-mode" : ""}"
|
||||
></div>
|
||||
@ -191,6 +201,12 @@ export class SideBarView extends LitElement implements LovelaceViewElement {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
hui-view-badges {
|
||||
display: block;
|
||||
margin: 12px 8px 20px 8px;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -1,153 +0,0 @@
|
||||
import "@material/mwc-button";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-icon-button";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
|
||||
@customElement("ha-dialog-show-audio-message")
|
||||
class HaDialogShowAudioMessage extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _currentMessage?: any;
|
||||
|
||||
@state() private _errorMsg?: string;
|
||||
|
||||
@state() private _loading: boolean = false;
|
||||
|
||||
@state() private _opened: boolean = false;
|
||||
|
||||
@state() private _blobUrl?: string;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closed=${this._closeDialog}
|
||||
heading=${this.hass.localize("ui.panel.mailbox.playback_title")}
|
||||
>
|
||||
${this._loading
|
||||
? html`<ha-circular-progress indeterminate></ha-circular-progress>`
|
||||
: html`<div class="icon">
|
||||
<ha-icon-button id="delicon" @click=${this._openDeleteDialog}>
|
||||
<ha-icon icon="hass:delete"></ha-icon>
|
||||
</ha-icon-button>
|
||||
</div>
|
||||
${
|
||||
this._currentMessage
|
||||
? html`<div id="transcribe">
|
||||
${this._currentMessage?.message}
|
||||
</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._errorMsg
|
||||
? html`<div class="error">${this._errorMsg}</div>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._blobUrl
|
||||
? html` <audio id="mp3" preload="none" controls autoplay>
|
||||
<source
|
||||
id="mp3src"
|
||||
src=${this._blobUrl}
|
||||
type="audio/mpeg"
|
||||
/>
|
||||
</audio>`
|
||||
: nothing
|
||||
}
|
||||
</div>`}
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
showDialog({ hass, message }) {
|
||||
this.hass = hass;
|
||||
this._errorMsg = undefined;
|
||||
this._currentMessage = message;
|
||||
this._opened = true;
|
||||
const platform = message.platform;
|
||||
if (platform.has_media) {
|
||||
this._loading = true;
|
||||
const url = `/api/mailbox/media/${platform.name}/${message.sha}`;
|
||||
this.hass
|
||||
.fetchWithAuth(url)
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
return response.blob();
|
||||
}
|
||||
return Promise.reject({
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
});
|
||||
})
|
||||
.then((blob) => {
|
||||
this._loading = false;
|
||||
this._blobUrl = window.URL.createObjectURL(blob);
|
||||
})
|
||||
.catch((err) => {
|
||||
this._loading = false;
|
||||
this._errorMsg = `Error loading audio: ${err.statusText}`;
|
||||
});
|
||||
} else {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private _openDeleteDialog() {
|
||||
if (confirm(this.hass.localize("ui.panel.mailbox.delete_prompt"))) {
|
||||
this._deleteSelected();
|
||||
}
|
||||
}
|
||||
|
||||
private _deleteSelected() {
|
||||
const msg = this._currentMessage;
|
||||
this.hass.callApi(
|
||||
"DELETE",
|
||||
`mailbox/delete/${msg.platform.name}/${msg.sha}`
|
||||
);
|
||||
this._closeDialog();
|
||||
}
|
||||
|
||||
private _closeDialog() {
|
||||
const mp3 = this.shadowRoot!.querySelector("#mp3")! as any;
|
||||
mp3.pause();
|
||||
this._currentMessage = undefined;
|
||||
this._errorMsg = undefined;
|
||||
this._loading = false;
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
.error {
|
||||
color: red;
|
||||
}
|
||||
p {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.icon {
|
||||
text-align: var(--float-end);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-dialog-show-audio-message": HaDialogShowAudioMessage;
|
||||
}
|
||||
}
|
@ -1,275 +0,0 @@
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "@material/mwc-button";
|
||||
import { formatDateTime } from "../../common/datetime/format_date_time";
|
||||
import "../../components/ha-card";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-tabs";
|
||||
import "@polymer/paper-tabs/paper-tab";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import { formatDuration } from "../../common/datetime/format_duration";
|
||||
|
||||
let registeredDialog = false;
|
||||
|
||||
interface MailboxMessage {
|
||||
info: {
|
||||
origtime: number;
|
||||
callerid: string;
|
||||
duration: string;
|
||||
};
|
||||
text: string;
|
||||
sha: string;
|
||||
}
|
||||
|
||||
@customElement("ha-panel-mailbox")
|
||||
class HaPanelMailbox extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public platforms?: any[];
|
||||
|
||||
@state() private _messages?: any[];
|
||||
|
||||
@state() private _currentPlatform: number = 0;
|
||||
|
||||
private _unsubEvents?;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed>
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
<div slot="title">${this.hass.localize("panel.mailbox")}</div>
|
||||
${!this._areTabsHidden(this.platforms)
|
||||
? html`<div sticky>
|
||||
<ha-tabs
|
||||
scrollable
|
||||
.selected=${this._currentPlatform}
|
||||
@iron-activate=${this._handlePlatformSelected}
|
||||
>
|
||||
${this.platforms?.map(
|
||||
(platform) =>
|
||||
html` <paper-tab data-entity=${platform}>
|
||||
${this._getPlatformName(platform)}
|
||||
</paper-tab>`
|
||||
)}
|
||||
</ha-tabs>
|
||||
</div>`
|
||||
: ""}
|
||||
</ha-top-app-bar-fixed>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
${!this._messages?.length
|
||||
? html`<div class="card-content empty">
|
||||
${this.hass.localize("ui.panel.mailbox.empty")}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._messages?.map(
|
||||
(message) =>
|
||||
html` <ha-list-item
|
||||
.message=${message}
|
||||
@click=${this._openMP3Dialog}
|
||||
twoline
|
||||
>
|
||||
<span>
|
||||
<span>${message.caller}</span>
|
||||
<span class="tip">
|
||||
${formatDuration(this.hass.locale, {
|
||||
seconds: message.duration,
|
||||
})}
|
||||
</span>
|
||||
</span>
|
||||
<span slot="secondary">
|
||||
<span class="date">${message.timestamp}</span> -
|
||||
${message.message}
|
||||
</span>
|
||||
</ha-list-item>`
|
||||
)}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (!registeredDialog) {
|
||||
registeredDialog = true;
|
||||
fireEvent(this, "register-dialog", {
|
||||
dialogShowEvent: "show-audio-message-dialog",
|
||||
dialogTag: "ha-dialog-show-audio-message",
|
||||
dialogImport: () => import("./ha-dialog-show-audio-message"),
|
||||
});
|
||||
}
|
||||
this.hassChanged = this.hassChanged.bind(this);
|
||||
this.hass.connection
|
||||
.subscribeEvents(this.hassChanged, "mailbox_updated")
|
||||
.then((unsub) => {
|
||||
this._unsubEvents = unsub;
|
||||
});
|
||||
this._computePlatforms().then((platforms) => {
|
||||
this.platforms = platforms;
|
||||
this.hassChanged();
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._unsubEvents) this._unsubEvents();
|
||||
}
|
||||
|
||||
hassChanged() {
|
||||
if (!this._messages) {
|
||||
this._messages = [];
|
||||
}
|
||||
this._getMessages().then((items) => {
|
||||
this._messages = items;
|
||||
});
|
||||
}
|
||||
|
||||
private _openMP3Dialog(ev) {
|
||||
const message: any = (ev.currentTarget! as any).message;
|
||||
fireEvent(this, "show-audio-message-dialog", {
|
||||
hass: this.hass,
|
||||
message: message,
|
||||
});
|
||||
}
|
||||
|
||||
private _getMessages() {
|
||||
const platform = this.platforms![this._currentPlatform];
|
||||
return this.hass
|
||||
.callApi<MailboxMessage[]>("GET", `mailbox/messages/${platform.name}`)
|
||||
.then((values) => {
|
||||
const platformItems: any[] = [];
|
||||
const arrayLength = values.length;
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
const datetime = formatDateTime(
|
||||
new Date(values[i].info.origtime * 1000),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
platformItems.push({
|
||||
timestamp: datetime,
|
||||
caller: values[i].info.callerid,
|
||||
message: values[i].text,
|
||||
sha: values[i].sha,
|
||||
duration: values[i].info.duration,
|
||||
platform: platform,
|
||||
});
|
||||
}
|
||||
return platformItems.sort((a, b) => b.timestamp - a.timestamp);
|
||||
});
|
||||
}
|
||||
|
||||
private _computePlatforms(): Promise<any[]> {
|
||||
return this.hass.callApi<any[]>("GET", "mailbox/platforms");
|
||||
}
|
||||
|
||||
private _handlePlatformSelected(ev) {
|
||||
const newPlatform = ev.detail.selected;
|
||||
if (newPlatform !== this._currentPlatform) {
|
||||
this._currentPlatform = newPlatform;
|
||||
this.hassChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private _areTabsHidden(platforms) {
|
||||
return !platforms || platforms.length < 2;
|
||||
}
|
||||
|
||||
private _getPlatformName(item) {
|
||||
const entity = `mailbox.${item.name}`;
|
||||
const stateObj = this.hass.states[entity.toLowerCase()];
|
||||
return stateObj.attributes.friendly_name;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
:host {
|
||||
-ms-user-select: initial;
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 16px;
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ha-tabs {
|
||||
margin-left: max(env(safe-area-inset-left), 24px);
|
||||
margin-right: max(env(safe-area-inset-right), 24px);
|
||||
margin-inline-start: max(env(safe-area-inset-left), 24px);
|
||||
margin-inline-end: max(env(safe-area-inset-right), 24px);
|
||||
--paper-tabs-selection-bar-color: #fff;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply --paper-font-title;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
@media all and (max-width: 450px) {
|
||||
.content {
|
||||
width: auto;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: 14px;
|
||||
}
|
||||
.date {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-panel-mailbox": HaPanelMailbox;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"show-audio-message-dialog": {
|
||||
hass: HomeAssistant;
|
||||
message: string;
|
||||
};
|
||||
}
|
||||
}
|
@ -364,6 +364,9 @@ class DialogTodoItemEditor extends LitElement {
|
||||
"ui.components.todo.item.confirm_delete.delete"
|
||||
),
|
||||
text: this.hass.localize("ui.components.todo.item.confirm_delete.prompt"),
|
||||
destructive: true,
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
});
|
||||
if (!confirm) {
|
||||
// Cancel
|
||||
|
@ -7,7 +7,6 @@
|
||||
"map": "Map",
|
||||
"logbook": "Logbook",
|
||||
"history": "History",
|
||||
"mailbox": "Mailbox",
|
||||
"todo": "To-do lists",
|
||||
"developer_tools": "Developer tools",
|
||||
"media_browser": "Media",
|
||||
@ -71,6 +70,11 @@
|
||||
"backup": {
|
||||
"upload_backup": "Upload backup"
|
||||
},
|
||||
"badge": {
|
||||
"entity": {
|
||||
"not_found": "[%key:ui::card::tile::not_found%]"
|
||||
}
|
||||
},
|
||||
"card": {
|
||||
"common": {
|
||||
"turn_on": "Turn on",
|
||||
@ -171,6 +175,7 @@
|
||||
"actions": {
|
||||
"resume_mowing": "Resume mowing",
|
||||
"start_mowing": "Start mowing",
|
||||
"pause": "Pause",
|
||||
"dock": "Return to dock"
|
||||
}
|
||||
},
|
||||
@ -1533,7 +1538,10 @@
|
||||
},
|
||||
"schedule": {
|
||||
"delete": "Delete item?",
|
||||
"confirm_delete": "Do you want to delete this item?"
|
||||
"confirm_delete": "Do you want to delete this item?",
|
||||
"edit_schedule_block": "Edit schedule block",
|
||||
"start": "Start",
|
||||
"end": "End"
|
||||
},
|
||||
"template": {
|
||||
"time": "[%key:ui::panel::developer-tools::tabs::templates::time%]",
|
||||
@ -4693,9 +4701,12 @@
|
||||
"title": "Change network channel",
|
||||
"new_channel": "New channel",
|
||||
"change_channel": "Change channel",
|
||||
"migration_warning": "Zigbee channel migration is an experimental feature and relies on devices on your network to support it. Device support for this feature varies and only a portion of your network may end up migrating!",
|
||||
"migration_warning": "Zigbee channel migration is an experimental feature and relies on devices on your network to support it. Device support for this feature varies and only a portion of your network may end up migrating! It may take up to an hour for changes to propagate to all devices.",
|
||||
"description": "Change your Zigbee channel only after you have eliminated all other sources of 2.4GHz interference by using a USB extension cable and moving your coordinator away from USB 3.0 devices and ports, SSDs, 2.4GHz WiFi networks on the same channel, motherboards, and so on.",
|
||||
"smart_explanation": "It is recommended to use the \"Smart\" option once your environment is optimized as opposed to manually choosing a channel, as it picks the best channel for you after scanning all Zigbee channels. This does not configure ZHA to automatically change channels in the future, it only changes the channel a single time.",
|
||||
"channel_has_been_changed": "Network channel has been changed",
|
||||
"devices_will_rejoin": "Devices will re-join the network over time. This may take a few minutes."
|
||||
"devices_will_rejoin": "Devices will re-join the network over time. This may take a few minutes.",
|
||||
"channel_auto": "Smart"
|
||||
}
|
||||
},
|
||||
"zwave_js": {
|
||||
@ -5492,8 +5503,8 @@
|
||||
"saved": "Saved",
|
||||
"reload": "Reload",
|
||||
"lovelace_changed": "Your dashboard was updated, do you want to load the updated config in the editor and lose your current changes?",
|
||||
"confirm_remove_config_title": "Are you sure you want to remove your dashboard configuration?",
|
||||
"confirm_remove_config_text": "We will automatically generate your dashboard views with your areas and devices if you remove your dashboard configuration.",
|
||||
"confirm_delete_config_title": "Delete dashboard configuration?",
|
||||
"confirm_delete_config_text": "This dashboard will be permanently deleted. The dashboard will be automatically regenerated to display your areas, devices and entities.",
|
||||
"confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?",
|
||||
"confirm_unsaved_comments": "Your configuration might contains comment(s), these will not be saved. Do you want to continue?",
|
||||
"error_parse_yaml": "Unable to parse YAML: {error}",
|
||||
@ -5577,6 +5588,10 @@
|
||||
"tab_layout": "Layout",
|
||||
"visibility": {
|
||||
"explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown."
|
||||
},
|
||||
"layout": {
|
||||
"full_width": "Full width card",
|
||||
"full_width_helper": "Take up the full width of the section whatever its size"
|
||||
}
|
||||
},
|
||||
"edit_badge": {
|
||||
@ -5646,7 +5661,10 @@
|
||||
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
|
||||
"settings": {
|
||||
"title": "Title",
|
||||
"title_helper": "The title will appear at the top of section. Leave empty to hide the title."
|
||||
"title_helper": "The title will appear at the top of section. Leave empty to hide the title.",
|
||||
"column_span": "Size",
|
||||
"column_span_unit": "columns",
|
||||
"column_span_helper": "The size may be smaller if less columns are displayed (e.g. on mobile devices)."
|
||||
},
|
||||
"visibility": {
|
||||
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
|
||||
@ -5948,9 +5966,9 @@
|
||||
"paste": "Paste from clipboard",
|
||||
"paste_description": "Paste a {type} card from the clipboard",
|
||||
"refresh_interval": "Refresh interval",
|
||||
"show_icon": "Show icon?",
|
||||
"show_name": "Show name?",
|
||||
"show_state": "Show state?",
|
||||
"show_icon": "Show icon",
|
||||
"show_name": "Show name",
|
||||
"show_state": "Show state",
|
||||
"tap_action": "Tap behavior",
|
||||
"title": "Title",
|
||||
"theme": "Theme",
|
||||
@ -6090,11 +6108,11 @@
|
||||
"appearance": "Appearance",
|
||||
"show_entity_picture": "Show entity picture",
|
||||
"state_content": "State content",
|
||||
"display_type": "Display type",
|
||||
"display_type_options": {
|
||||
"minimal": "Minimal (icon only)",
|
||||
"standard": "Standard (icon and state)",
|
||||
"complete": "Complete (icon, name and state)"
|
||||
"displayed_elements": "Displayed elements",
|
||||
"displayed_elements_options": {
|
||||
"icon": "Icon",
|
||||
"name": "Name",
|
||||
"state": "State"
|
||||
}
|
||||
},
|
||||
"generic": {
|
||||
@ -6355,12 +6373,6 @@
|
||||
},
|
||||
"reload_lovelace": "Reload UI"
|
||||
},
|
||||
"mailbox": {
|
||||
"empty": "You do not have any messages",
|
||||
"playback_title": "Message playback",
|
||||
"delete_prompt": "Delete this message?",
|
||||
"delete_button": "Delete"
|
||||
},
|
||||
"media-browser": {
|
||||
"error": {
|
||||
"player_not_exist": "Media player {name} does not exist"
|
||||
@ -6857,6 +6869,7 @@
|
||||
"title": "Template",
|
||||
"description": "Templates are rendered using the Jinja2 template engine with some Home Assistant specific extensions.",
|
||||
"editor": "Template editor",
|
||||
"result": "Result",
|
||||
"reset": "Reset to demo template",
|
||||
"confirm_reset": "Do you want to reset your current template back to the demo template?",
|
||||
"confirm_clear": "Do you want to clear your current template?",
|
||||
@ -7145,7 +7158,8 @@
|
||||
"charts": {
|
||||
"stat_house_energy_meter": "Total energy consumption",
|
||||
"solar": "Solar",
|
||||
"by_device": "Consumption by device"
|
||||
"by_device": "Consumption by device",
|
||||
"untracked_consumption": "Untracked consumption"
|
||||
},
|
||||
"cards": {
|
||||
"energy_usage_graph_title": "Energy usage",
|
||||
|
Loading…
x
Reference in New Issue
Block a user