Merge branch 'dev' into break-out-assist-chat

This commit is contained in:
Paulus Schoutsen 2024-08-26 09:57:07 +02:00 committed by GitHub
commit 199b7d9bc3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
94 changed files with 2416 additions and 2436 deletions

View File

@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.3.4
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.4
uses: actions/upload-artifact@v4.3.6
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.3.4
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.4
uses: actions/upload-artifact@v4.3.6
with:
name: translations
path: translations.tar.gz

View File

@ -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

925
.yarn/releases/yarn-4.4.0.cjs vendored Executable file

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.3.1.cjs
yarnPath: .yarn/releases/yarn-4.4.0.cjs

View File

@ -29,7 +29,7 @@ const compressDistZopfli = (rootDir, modernDir) =>
[
`${rootDir}/**/${filesGlob}`,
`!${modernDir}/**/${filesGlob}`,
`!${rootDir}/sw-modern.js`,
`!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`,
],
{ base: rootDir }

View File

@ -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)
)
);
}

View File

@ -2,8 +2,8 @@
import { deleteAsync } from "del";
import gulp from "gulp";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, relative } from "node:path";
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { basename, join, relative } from "node:path";
import { injectManifest } from "workbox-build";
import paths from "../paths.cjs";
@ -41,10 +41,11 @@ gulp.task("gen-service-worker-app-prod", () =>
await readFile(join(outPath, "manifest.json"), "utf-8")
);
const swSrc = join(paths.app_output_root, manifest["service-worker.js"]);
const swDest = join(paths.app_output_root, `sw-${build}.js`);
const buildDir = relative(paths.app_output_root, outPath);
const { warnings } = await injectManifest({
swSrc,
swDest: join(paths.app_output_root, `sw-${build}.js`),
swDest,
injectionPoint: "__WB_MANIFEST__",
// Files that mach this pattern will be considered unique and skip revision check
// ignore JS files + translation files
@ -76,6 +77,11 @@ gulp.task("gen-service-worker-app-prod", () =>
);
}
await deleteAsync(`${swSrc}?(.map)`);
// Needed to install new SW from a cached HTML
if (build === "modern") {
const swOld = join(paths.app_output_root, "service_worker.js");
await symlink(basename(swDest), swOld);
}
})
)
);

View File

@ -63,33 +63,42 @@
align-items: center;
}
#ha-launch-screen svg {
width: 170px;
width: 112px;
flex-shrink: 0;
}
#ha-launch-screen .ha-launch-screen-spacer {
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(env(safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
flex: 1;
padding-top: 48px;
}
.ohf-logo {
color: grey;
font-size: 12px;
margin-bottom: 16px;
margin: max(env(safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;
opacity: .66;
}
@media (prefers-color-scheme: dark) {
.ohf-logo {
filter: invert(1);
}
}
</style>
</head>
<body>
<div id="ha-launch-screen">
<div class="ha-launch-screen-spacer"></div>
<div class="ha-launch-screen-spacer-top"></div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
<path fill="#18BCF2" d="M240 224.762a15 15 0 0 1-15 15H15a15 15 0 0 1-15-15v-90c0-8.25 4.77-19.769 10.61-25.609l98.78-98.7805c5.83-5.83 15.38-5.83 21.21 0l98.79 98.7895c5.83 5.83 10.61 17.36 10.61 25.61v90-.01Z"/>
<path fill="#F2F4F9" d="m107.27 239.762-40.63-40.63c-2.09.72-4.32 1.13-6.64 1.13-11.3 0-20.5-9.2-20.5-20.5s9.2-20.5 20.5-20.5 20.5 9.2 20.5 20.5c0 2.33-.41 4.56-1.13 6.65l31.63 31.63v-115.88c-6.8-3.3395-11.5-10.3195-11.5-18.3895 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5c0 8.07-4.7 15.05-11.5 18.3895v81.27l31.46-31.46c-.62-1.96-.96-4.04-.96-6.2 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5-9.2 20.5-20.5 20.5c-2.5 0-4.88-.47-7.09-1.29L129 208.892v30.88z"/>
</svg>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer-bottom"></div>
<div class="ohf-logo">
a project from
<img src="/static/icons/ohf.svg" alt="Open Home Foundation" height="32">
<img src="/static/images/ohf-badge.svg" alt="Home Assistant is a project by the Open Home Foundation" height="46">
</div>
</div>
<ha-demo></ha-demo>

View File

@ -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",

View File

@ -8,6 +8,7 @@ import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import "../../components/demo-cards";
import { mockIcons } from "../../../../demo/src/stubs/icons";
import { ClimateEntityFeature } from "../../../../src/data/climate";
const ENTITIES = [
getEntity("switch", "tv_outlet", "on", {
@ -60,6 +61,36 @@ const ENTITIES = [
CoverEntityFeature.OPEN_TILT +
CoverEntityFeature.STOP_TILT,
}),
getEntity("input_number", "counter", "1.0", {
friendly_name: "Counter",
initial: 0,
min: 0,
max: 100,
step: 1,
mode: "slider",
}),
getEntity("climate", "dual_thermostat", "heat/cool", {
friendly_name: "Dual thermostat",
hvac_modes: ["off", "cool", "heat_cool", "auto", "dry", "fan_only"],
min_temp: 7,
max_temp: 35,
fan_modes: ["on_low", "on_high", "auto_low", "auto_high", "off"],
preset_modes: ["home", "eco", "away"],
swing_modes: ["auto", "1", "2", "3", "off"],
current_temperature: 23,
target_temp_high: 24,
target_temp_low: 21,
fan_mode: "auto_low",
preset_mode: "home",
swing_mode: "auto",
supported_features:
ClimateEntityFeature.TURN_ON +
ClimateEntityFeature.TURN_OFF +
ClimateEntityFeature.SWING_MODE +
ClimateEntityFeature.PRESET_MODE +
ClimateEntityFeature.FAN_MODE +
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
}),
];
const CONFIGS = [
@ -193,6 +224,25 @@ const CONFIGS = [
- type: "cover-tilt"
`,
},
{
heading: "Number buttons feature",
config: `
- type: tile
entity: input_number.counter
features:
- type: numeric-input
style: buttons
`,
},
{
heading: "Dual thermostat feature",
config: `
- type: tile
entity: climate.dual_thermostat
features:
- type: target-temperature
`,
},
];
@customElement("demo-lovelace-tile-card")

View File

@ -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.17.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.29.1",
"@codemirror/view": "6.32.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.12.5",
"@formatjs/intl-displaynames": "6.6.8",
@ -49,11 +49,11 @@
"@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",
"@lit-labs/virtualizer": "2.0.13",
"@lit-labs/virtualizer": "2.0.14",
"@lrnwebcomponents/simple-tooltip": "8.0.2",
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
@ -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.4",
"@vaadin/vaadin-themable-mixin": "24.4.4",
"@vaadin/combo-box": "24.4.5",
"@vaadin/vaadin-themable-mixin": "24.4.5",
"@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.37.1",
"core-js": "3.38.1",
"cropperjs": "1.6.2",
"date-fns": "3.6.0",
"date-fns-tz": "3.1.3",
@ -117,20 +117,20 @@
"leaflet": "1.9.4",
"leaflet-draw": "1.0.4",
"lit": "2.8.0",
"luxon": "3.4.4",
"marked": "13.0.3",
"luxon": "3.5.0",
"marked": "14.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "0.3.2",
"punycode": "2.3.1",
"qr-scanner": "1.4.2",
"qrcode": "1.5.3",
"qrcode": "1.5.4",
"roboto-fontface": "0.10.0",
"rrule": "2.8.1",
"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",
@ -149,18 +149,18 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.24.9",
"@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.0",
"@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.13.4",
"@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",
@ -185,8 +185,8 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@web/dev-server": "0.1.38",
"@web/dev-server-rollup": "0.4.1",
"babel-loader": "9.1.3",
@ -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.7",
"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.3.1"
"packageManager": "yarn@4.4.0"
}

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="b" data-name="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760.69 138.69">
<g id="c" data-name="Layer_1">
<g>
<g>
<path d="M136.22,68.35c3.31-.05,6.29.72,8.92,2.31s4.67,3.77,6.11,6.55,2.14,5.89,2.11,9.33c.03,3.44-.68,6.55-2.12,9.34s-3.49,4.98-6.13,6.56-5.62,2.36-8.93,2.31c-3.33,0-6.29-.78-8.88-2.33s-4.6-3.71-6.02-6.48-2.13-5.9-2.13-9.4c-.03-2.56.38-4.98,1.22-7.24s2.01-4.21,3.5-5.82,3.31-2.88,5.45-3.8,4.45-1.36,6.91-1.32ZM136.27,98.35c3.09,0,5.56-1.07,7.41-3.2s2.77-5,2.77-8.61-.91-6.52-2.73-8.65-4.3-3.19-7.44-3.19-5.63,1.06-7.46,3.19-2.75,5.01-2.75,8.65.91,6.52,2.74,8.64,4.32,3.18,7.48,3.18Z"/>
<path d="M184.16,80.53c0,3.47-1.06,6.27-3.18,8.41s-4.98,3.21-8.59,3.21h-7.45v12h-6.56v-35.18h14.06c3.64,0,6.5,1.04,8.59,3.11s3.13,4.89,3.13,8.45ZM177.25,80.39c0-1.64-.52-2.98-1.56-4.03s-2.52-1.57-4.44-1.57h-6.3v11.65h6.26c1.95,0,3.45-.55,4.49-1.65s1.56-2.57,1.56-4.39Z"/>
<path d="M210.82,98.02v6.14h-22.03v-35.18h21.98v6.19h-15.42v8.3h13.78v5.81h-13.78v8.74h15.47Z"/>
<path d="M246.95,68.98v35.18h-6.49l-16.08-24.77v24.77h-6.52v-35.18h6.52l16.08,24.77v-24.77h6.49Z"/>
<path d="M266.45,68.98h6.56v14.44l14.7.05v-14.48h6.63v35.18h-6.63v-14.84l-14.7-.09v14.93h-6.56v-35.18Z"/>
<path d="M316.41,68.35c3.31-.05,6.29.72,8.92,2.31s4.67,3.77,6.11,6.55,2.14,5.89,2.11,9.33c.03,3.44-.68,6.55-2.12,9.34s-3.49,4.98-6.13,6.56-5.62,2.36-8.93,2.31c-3.33,0-6.29-.78-8.88-2.33s-4.6-3.71-6.02-6.48-2.13-5.9-2.13-9.4c-.03-2.56.38-4.98,1.22-7.24s2.01-4.21,3.5-5.82,3.31-2.88,5.45-3.8,4.45-1.36,6.91-1.32ZM316.46,98.35c3.09,0,5.56-1.07,7.41-3.2s2.77-5,2.77-8.61-.91-6.52-2.73-8.65-4.3-3.19-7.44-3.19-5.63,1.06-7.46,3.19-2.75,5.01-2.75,8.65.91,6.52,2.74,8.64,4.32,3.18,7.48,3.18Z"/>
<path d="M373.66,68.98v35.18h-6.45v-20.55l-8.11,20.55h-6.23l-8.02-20.39v20.39h-6.28v-35.18h6.28l11.13,27.54,11.23-27.54h6.45Z"/>
<path d="M402.87,98.02v6.14h-22.03v-35.18h21.98v6.19h-15.42v8.3h13.78v5.81h-13.78v8.74h15.47Z"/>
<path d="M427.83,75.12v8.93h13.01l-.05,5.91h-12.96v14.2h-6.52v-35.18h21.98l-.05,6.14h-15.42Z"/>
<path d="M463.16,68.35c3.31-.05,6.29.72,8.92,2.31s4.67,3.77,6.11,6.55,2.14,5.89,2.11,9.33c.03,3.44-.68,6.55-2.12,9.34s-3.49,4.98-6.13,6.56-5.62,2.36-8.93,2.31c-3.33,0-6.29-.78-8.88-2.33s-4.6-3.71-6.02-6.48-2.13-5.9-2.13-9.4c-.03-2.56.38-4.98,1.22-7.24s2.01-4.21,3.5-5.82,3.31-2.88,5.45-3.8,4.45-1.36,6.91-1.32ZM463.21,98.35c3.09,0,5.56-1.07,7.41-3.2s2.77-5,2.77-8.61-.91-6.52-2.73-8.65-4.3-3.19-7.44-3.19-5.63,1.06-7.46,3.19-2.75,5.01-2.75,8.65.91,6.52,2.74,8.64,4.32,3.18,7.48,3.18Z"/>
<path d="M485,68.98h6.56v22.12c0,2.31.72,4.12,2.16,5.43s3.3,1.96,5.58,1.96,4.08-.67,5.58-2.02,2.25-3.13,2.25-5.37v-22.12h6.52v22.31c0,2.08-.38,3.98-1.14,5.7s-1.79,3.14-3.09,4.25-2.82,1.98-4.56,2.59-3.59.91-5.55.91c-2.59,0-4.96-.52-7.1-1.55s-3.88-2.58-5.2-4.65-1.99-4.49-1.99-7.25v-22.31Z"/>
<path d="M549.63,68.98v35.18h-6.49l-16.08-24.77v24.77h-6.52v-35.18h6.52l16.08,24.77v-24.77h6.49Z"/>
<path d="M586.9,86.58c.05,3.34-.71,6.37-2.27,9.08s-3.7,4.82-6.42,6.32-5.73,2.23-9.02,2.18h-12.42v-35.18h12.42c2.45-.03,4.78.39,6.98,1.28s4.1,2.1,5.68,3.66,2.84,3.43,3.75,5.64,1.35,4.55,1.3,7.03ZM579.99,86.58c0-3.39-1-6.16-3.01-8.3s-4.62-3.21-7.84-3.21h-5.81v23.04h5.81c3.27,0,5.89-1.06,7.88-3.19s2.98-4.91,2.98-8.34Z"/>
<path d="M609.16,96.19h-12.73l-2.79,7.97h-6.82l12.68-35.18h6.63l12.66,35.18h-6.96l-2.67-7.97ZM607.24,90.73l-4.43-12.87-4.45,12.87h8.88Z"/>
<path d="M642.87,75.17h-9.89v28.99h-6.56v-28.99h-9.94v-6.19h26.39v6.19Z"/>
<path d="M647.06,104.16v-35.18h6.56v35.18h-6.56Z"/>
<path d="M675.71,68.35c3.31-.05,6.29.72,8.92,2.31s4.67,3.77,6.11,6.55,2.14,5.89,2.11,9.33c.03,3.44-.68,6.55-2.12,9.34s-3.49,4.98-6.13,6.56-5.62,2.36-8.93,2.31c-3.33,0-6.29-.78-8.88-2.33s-4.6-3.71-6.02-6.48-2.13-5.9-2.13-9.4c-.03-2.56.38-4.98,1.22-7.24s2.01-4.21,3.5-5.82,3.31-2.88,5.45-3.8,4.45-1.36,6.91-1.32ZM675.76,98.35c3.09,0,5.56-1.07,7.41-3.2s2.77-5,2.77-8.61-.91-6.52-2.73-8.65-4.3-3.19-7.44-3.19-5.63,1.06-7.46,3.19-2.75,5.01-2.75,8.65.91,6.52,2.74,8.64,4.32,3.18,7.48,3.18Z"/>
<path d="M726.96,68.98v35.18h-6.49l-16.08-24.77v24.77h-6.52v-35.18h6.52l16.08,24.77v-24.77h6.49Z"/>
</g>
<g>
<path d="M94.34,79.34c0,2.75-2.25,5-5,5h-50c-2.75,0-5-2.25-5-5v-20c0-2.75,1.59-6.59,3.54-8.54l22.93-22.93c1.94-1.94,5.13-1.94,7.07,0l22.93,22.93c1.94,1.94,3.54,5.79,3.54,8.54v20Z"/>
<g>
<rect x="34.34" y="94.34" width="60" height="10" rx="2.5" ry="2.5"/>
<rect x="34.34" y="94.34" width="10" height="20" rx="1.56" ry="1.56"/>
<rect x="84.34" y="94.34" width="10" height="20" rx="1.56" ry="1.56"/>
</g>
</g>
<path d="M735.34,3c12.32,0,22.34,10.02,22.34,22.34v88c0,12.32-10.02,22.34-22.34,22.34H25.34c-12.32,0-22.34-10.02-22.34-22.34V25.34C3,13.02,13.02,3,25.34,3h710M735.34,0H25.34C11.37,0,0,11.37,0,25.34v88c0,13.98,11.37,25.34,25.34,25.34h710c13.97,0,25.34-11.37,25.34-25.34V25.34c0-13.98-11.37-25.34-25.34-25.34h0Z"/>
<g>
<path d="M120.98,36.79h2.95v7.26l7.66.02v-7.29h2.97v17.37h-2.97v-7.47l-7.66-.02v7.49h-2.95v-17.37Z"/>
<path d="M146.97,36.47c1.63,0,3.09.39,4.37,1.16s2.28,1.84,2.99,3.2,1.06,2.9,1.06,4.61c.02,1.7-.32,3.24-1.04,4.62s-1.72,2.47-3.02,3.25-2.75,1.16-4.36,1.14c-1.62.02-3.08-.36-4.37-1.14s-2.29-1.86-3-3.24-1.05-2.91-1.03-4.61c0-1.27.2-2.47.61-3.58s.99-2.08,1.72-2.88,1.63-1.42,2.68-1.88,2.18-.67,3.39-.66ZM146.99,51.57c1.6,0,2.89-.56,3.85-1.67s1.45-2.6,1.45-4.45-.48-3.32-1.45-4.43-2.25-1.66-3.85-1.66-2.89.55-3.86,1.66-1.45,2.58-1.45,4.43.48,3.34,1.44,4.46,2.25,1.67,3.88,1.67Z"/>
<path d="M176.51,36.79v17.37h-2.89v-10.78l-4.29,10.78h-2.81l-4.25-10.71v10.71h-2.84v-17.37h2.84l5.66,13.92,5.69-13.92h2.89Z"/>
<path d="M192.41,51.37v2.79h-10.78v-17.37h10.78v2.81h-7.83v4.5h7v2.61h-7v4.66h7.83Z"/>
<path d="M213.93,50.11h-6.61l-1.43,4.04h-3.04l6.27-17.37h3.04l6.29,17.37h-3.11l-1.41-4.04ZM213.07,47.62l-2.43-6.95-2.45,6.95h4.88Z"/>
<path d="M226.96,36.47c1.59,0,2.91.39,3.96,1.16s1.7,1.81,1.94,3.1l-2.78.76c-.16-.74-.52-1.32-1.09-1.72s-1.27-.6-2.11-.6c-.9,0-1.61.21-2.14.64s-.79,1-.79,1.71c0,1.12.7,1.85,2.09,2.18l2.84.71c1.46.38,2.56.98,3.29,1.81s1.09,1.84,1.09,3.05c0,1.55-.56,2.8-1.68,3.76s-2.63,1.44-4.51,1.44c-1.7,0-3.13-.4-4.3-1.2-1.15-.83-1.84-1.92-2.05-3.28l2.78-.72c.1.77.48,1.37,1.14,1.8s1.5.65,2.53.65,1.76-.21,2.32-.62.84-.98.84-1.69c0-1.12-.7-1.85-2.09-2.21l-2.84-.69c-1.46-.33-2.55-.92-3.28-1.77s-1.1-1.88-1.1-3.11c0-1.53.54-2.78,1.63-3.74s2.52-1.44,4.29-1.44Z"/>
<path d="M242.38,36.47c1.59,0,2.91.39,3.96,1.16s1.7,1.81,1.94,3.1l-2.78.76c-.16-.74-.52-1.32-1.09-1.72s-1.27-.6-2.11-.6c-.9,0-1.61.21-2.14.64s-.79,1-.79,1.71c0,1.12.7,1.85,2.09,2.18l2.84.71c1.46.38,2.56.98,3.29,1.81s1.09,1.84,1.09,3.05c0,1.55-.56,2.8-1.68,3.76s-2.63,1.44-4.51,1.44c-1.7,0-3.13-.4-4.3-1.2-1.15-.83-1.84-1.92-2.05-3.28l2.78-.72c.1.77.48,1.37,1.14,1.8s1.5.65,2.53.65,1.76-.21,2.32-.62.84-.98.84-1.69c0-1.12-.7-1.85-2.09-2.21l-2.84-.69c-1.46-.33-2.55-.92-3.28-1.77s-1.1-1.88-1.1-3.11c0-1.53.54-2.78,1.63-3.74s2.52-1.44,4.29-1.44Z"/>
<path d="M252.68,54.16v-17.37h2.95v17.37h-2.95Z"/>
<path d="M265.82,36.47c1.59,0,2.91.39,3.96,1.16s1.7,1.81,1.94,3.1l-2.78.76c-.16-.74-.52-1.32-1.09-1.72s-1.27-.6-2.11-.6c-.9,0-1.61.21-2.14.64s-.79,1-.79,1.71c0,1.12.7,1.85,2.09,2.18l2.84.71c1.46.38,2.56.98,3.29,1.81s1.09,1.84,1.09,3.05c0,1.55-.56,2.8-1.68,3.76s-2.63,1.44-4.51,1.44c-1.7,0-3.13-.4-4.3-1.2-1.15-.83-1.84-1.92-2.05-3.28l2.78-.72c.1.77.48,1.37,1.14,1.8s1.5.65,2.53.65,1.76-.21,2.32-.62.84-.98.84-1.69c0-1.12-.7-1.85-2.09-2.21l-2.84-.69c-1.46-.33-2.55-.92-3.28-1.77s-1.1-1.88-1.1-3.11c0-1.53.54-2.78,1.63-3.74s2.52-1.44,4.29-1.44Z"/>
<path d="M287.47,39.57h-4.97v14.58h-2.95v-14.58h-4.97v-2.79h12.9v2.79Z"/>
<path d="M298.87,50.11h-6.61l-1.43,4.04h-3.04l6.27-17.37h3.04l6.29,17.37h-3.11l-1.41-4.04ZM298.01,47.62l-2.43-6.95-2.45,6.95h4.88Z"/>
<path d="M320.89,36.79v17.37h-2.93l-8.25-12.67v12.67h-2.93v-17.37h2.93l8.25,12.65v-12.65h2.93Z"/>
<path d="M337.31,39.57h-4.97v14.58h-2.95v-14.58h-4.97v-2.79h12.9v2.79Z"/>
<path d="M348.75,54.16v-17.14h2.05v17.14h-2.05Z"/>
<path d="M360.95,36.72c1.55,0,2.82.38,3.81,1.14,1,.74,1.61,1.72,1.82,2.95l-1.95.52c-.16-.87-.56-1.54-1.23-2.02s-1.5-.71-2.5-.71c-1.08,0-1.95.27-2.6.8s-.97,1.24-.97,2.13c0,1.36.84,2.26,2.52,2.71l2.9.73c1.37.34,2.41.9,3.11,1.68s1.05,1.74,1.05,2.9c0,1.46-.53,2.64-1.6,3.54s-2.49,1.36-4.28,1.36c-1.61,0-2.95-.37-4.03-1.12s-1.72-1.76-1.95-3.06l1.98-.55c.13.88.55,1.56,1.25,2.06s1.63.75,2.77.75,2.12-.26,2.79-.77,1.02-1.22,1.02-2.14c0-1.44-.84-2.36-2.52-2.77l-2.88-.71c-1.38-.34-2.42-.9-3.12-1.69s-1.05-1.75-1.05-2.9c0-1.44.52-2.61,1.56-3.51s2.4-1.35,4.09-1.35Z"/>
<path d="M388.35,49.75h-7.54l-1.59,4.4h-2.07l6.25-17.14h2.36l6.31,17.14h-2.15l-1.57-4.4ZM387.73,48.05l-3.09-8.71-3.2,8.71h6.29Z"/>
<path d="M415.46,42.47c0,1.6-.5,2.91-1.5,3.95s-2.32,1.56-3.97,1.56h-4.53v6.18h-2.05v-17.14h6.6c1.67,0,3,.49,3.98,1.47s1.47,2.31,1.47,3.98ZM413.31,42.42c0-1.07-.32-1.92-.95-2.56s-1.51-.96-2.64-.96h-4.26v7.24h4.17c1.15,0,2.06-.34,2.71-1.02s.98-1.58.98-2.7Z"/>
<path d="M428.37,46.9l3.43,7.26h-2.31l-3.18-6.95h-4.76v6.95h-2.05v-17.14h6.54c1.81,0,3.22.45,4.24,1.35s1.53,2.14,1.53,3.72c0,1.22-.3,2.26-.9,3.1s-1.44,1.41-2.53,1.7ZM429.64,42.12c0-1.01-.32-1.81-.95-2.38s-1.52-.86-2.66-.86h-4.5v6.47h4.53c1.15,0,2.03-.28,2.64-.85s.92-1.36.92-2.38Z"/>
<path d="M443.34,36.74c1.18-.02,2.28.2,3.31.65s1.9,1.07,2.62,1.85,1.28,1.73,1.69,2.83.6,2.27.59,3.52c.02,1.67-.33,3.19-1.03,4.54s-1.68,2.42-2.95,3.19-2.68,1.14-4.25,1.12c-1.59,0-3-.38-4.25-1.13s-2.21-1.81-2.89-3.15-1.03-2.87-1.03-4.57c-.02-1.67.32-3.18,1.02-4.53s1.67-2.42,2.93-3.2,2.68-1.15,4.24-1.13ZM443.34,52.45c1.8,0,3.26-.64,4.38-1.91s1.68-2.92,1.68-4.95-.56-3.71-1.68-4.98-2.58-1.9-4.38-1.9-3.28.63-4.4,1.9-1.68,2.93-1.68,4.98.56,3.69,1.68,4.96,2.59,1.9,4.39,1.9Z"/>
<path d="M464.3,37.02v12.42c0,1.49-.47,2.71-1.41,3.64s-2.17,1.39-3.71,1.39c-1.56,0-2.76-.46-3.61-1.37s-1.27-2.13-1.27-3.69v-.55h1.98v.55c0,1.18.29,1.99.86,2.45.59.45,1.26.67,2.02.67.93,0,1.68-.26,2.24-.78.57-.52.85-1.32.85-2.39v-12.35h2.05Z"/>
<path d="M479.86,52.23v1.93h-10.35v-17.14h10.33v1.95h-8.31v5.67h7.53v1.8h-7.53v5.79h8.33Z"/>
<path d="M496.97,42.42c-.36-1.15-1.01-2.06-1.93-2.71s-2.02-.98-3.3-.98c-1.79,0-3.24.63-4.35,1.89s-1.66,2.92-1.66,4.96.55,3.72,1.66,4.96,2.55,1.86,4.33,1.86c1.27,0,2.39-.31,3.35-.94.95-.63,1.61-1.44,1.98-2.45l1.93.83c-.55,1.41-1.48,2.53-2.78,3.36-1.31.81-2.81,1.22-4.51,1.22-2.4,0-4.35-.81-5.84-2.43s-2.24-3.75-2.24-6.4c0-1.74.34-3.28,1.02-4.62s1.64-2.39,2.88-3.13,2.65-1.11,4.25-1.11c1.8,0,3.33.46,4.59,1.38,1.26.91,2.12,2.1,2.57,3.59l-1.93.71Z"/>
<path d="M512.92,38.95h-5.12v15.21h-2.05v-15.21h-5.16v-1.93h12.33v1.93Z"/>
<path d="M536.4,49.66c0,1.39-.49,2.48-1.46,3.29s-2.27,1.21-3.9,1.21h-6.72v-17.12h6.58c1.65,0,2.95.41,3.89,1.24s1.41,1.93,1.41,3.32c0,.92-.21,1.72-.64,2.4s-1.02,1.2-1.79,1.57c.81.37,1.45.91,1.92,1.62s.7,1.53.7,2.47ZM526.3,38.81v5.97h4.54c1.03,0,1.85-.27,2.46-.81s.92-1.24.92-2.09c0-.91-.31-1.65-.93-2.22s-1.45-.85-2.5-.85h-4.5ZM534.45,49.39c0-.91-.32-1.64-.95-2.17s-1.44-.8-2.46-.8h-4.74v5.95h4.74c1.04,0,1.86-.27,2.48-.81s.92-1.26.92-2.16Z"/>
<path d="M541.07,37.02l4.54,8.1,4.54-8.1h2.24l-5.76,10.05v7.09h-2.05v-7.09l-5.83-10.05h2.31Z"/>
<path d="M574.27,38.95h-5.12v15.21h-2.05v-15.21h-5.16v-1.93h12.33v1.93Z"/>
<path d="M577.95,37.02h2.05v7.55l8.74.02v-7.58h2.05v17.14h-2.05v-7.76l-8.74-.02v7.79h-2.05v-17.14Z"/>
<path d="M606.55,52.23v1.93h-10.35v-17.14h10.33v1.95h-8.31v5.67h7.53v1.8h-7.53v5.79h8.33Z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20240731.0"
version = "20240809.0"
license = {text = "Apache-2.0"}
description = "The Home Assistant frontend"
readme = "README.md"

View File

@ -37,8 +37,7 @@
{
"description": "Group tsparticles engine and presets",
"groupName": "tsparticles",
"matchPackageNames": ["tsparticles-engine"],
"matchPackagePrefixes": ["tsparticles-preset-"]
"matchPackageNames": ["tsparticles-engine", "tsparticles-preset-{/,}**"]
},
{
"description": "Group date-fns with dependent timezone package",
@ -48,8 +47,8 @@
{
"description": "Group and temporarily disable WDS packages",
"groupName": "Web Dev Server",
"matchPackagePrefixes": ["@web/dev-server"],
"enabled": false
"enabled": false,
"matchPackageNames": ["@web/dev-server{/,}**"]
}
]
}

View File

@ -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,

View File

@ -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",

View File

@ -81,6 +81,9 @@ class HaEntityStatePicker extends LitElement {
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, attribute: "allow-name" }) public allowName =
false;
@property() public label?: string;
@property() public value?: string[] | string;
@ -95,43 +98,55 @@ class HaEntityStatePicker extends LitElement {
return !(!changedProps.has("_opened") && this._opened);
}
private options = memoizeOne((entityId?: string, stateObj?: HassEntity) => {
const domain = entityId ? computeDomain(entityId) : undefined;
return [
{
label: this.hass.localize("ui.components.state-content-picker.state"),
value: "state",
},
{
label: this.hass.localize(
"ui.components.state-content-picker.last_changed"
),
value: "last_changed",
},
{
label: this.hass.localize(
"ui.components.state-content-picker.last_updated"
),
value: "last_updated",
},
...(domain
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
).map((content) => ({
label: this.hass.localize(
`ui.components.state-content-picker.${content}`
),
value: content,
}))
: []),
...Object.keys(stateObj?.attributes ?? {})
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
.map((attribute) => ({
value: attribute,
label: this.hass.formatEntityAttributeName(stateObj!, attribute),
})),
];
});
private options = memoizeOne(
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
const domain = entityId ? computeDomain(entityId) : undefined;
return [
{
label: this.hass.localize("ui.components.state-content-picker.state"),
value: "state",
},
...(allowName
? [
{
label: this.hass.localize(
"ui.components.state-content-picker.name"
),
value: "name",
},
]
: []),
{
label: this.hass.localize(
"ui.components.state-content-picker.last_changed"
),
value: "last_changed",
},
{
label: this.hass.localize(
"ui.components.state-content-picker.last_updated"
),
value: "last_updated",
},
...(domain
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
).map((content) => ({
label: this.hass.localize(
`ui.components.state-content-picker.${content}`
),
value: content,
}))
: []),
...Object.keys(stateObj?.attributes ?? {})
.filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
.map((attribute) => ({
value: attribute,
label: this.hass.formatEntityAttributeName(stateObj!, attribute),
})),
];
}
);
private _filter = "";
@ -146,7 +161,7 @@ class HaEntityStatePicker extends LitElement {
? this.hass.states[this.entityId]
: undefined;
const options = this.options(this.entityId, stateObj);
const options = this.options(this.entityId, stateObj, this.allowName);
const optionItems = options.filter(
(option) => !this._value.includes(option.value)
);

View File

@ -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",

View File

@ -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 => {

View File

@ -36,6 +36,7 @@ export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
.allowName=${this.selector.ui_state_content?.allow_name}
></ha-entity-state-content-picker>
`;
}

View File

@ -366,7 +366,9 @@ export const normalizeAutomationConfig = <
}
}
config.action = migrateAutomationAction(config.action || []);
if (config.action) {
config.action = migrateAutomationAction(config.action);
}
return config;
};

View File

@ -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,

View File

@ -13,6 +13,7 @@ export const ensureBadgeConfig = (
return {
type: "entity",
entity: config,
display_type: "complete",
};
}
if ("type" in config && config.type) {

View File

@ -461,6 +461,7 @@ export interface UiStateContentSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
ui_state_content: {
entity_id?: string;
allow_name?: boolean;
} | null;
}

View File

@ -101,10 +101,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;
return html`
<ha-dialog

View File

@ -42,30 +42,39 @@
width: 112px;
flex-shrink: 0;
}
#ha-launch-screen .ha-launch-screen-spacer {
#ha-launch-screen .ha-launch-screen-spacer-top {
flex: 1;
margin-top: calc( 2 * max(env(safe-area-inset-bottom), 48px) + 46px );
padding-top: 48px;
}
#ha-launch-screen .ha-launch-screen-spacer-bottom {
flex: 1;
padding-top: 48px;
}
.ohf-logo {
color: grey;
font-size: 12px;
margin-bottom: 16px;
margin: max(env(safe-area-inset-bottom), 48px) 0;
display: flex;
flex-direction: column;
align-items: center;
opacity: .66;
}
@media (prefers-color-scheme: dark) {
.ohf-logo {
filter: invert(1);
}
}
</style>
</head>
<body>
<div id="ha-launch-screen">
<div class="ha-launch-screen-spacer"></div>
<div class="ha-launch-screen-spacer-top"></div>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
<path fill="#18BCF2" d="M240 224.762a15 15 0 0 1-15 15H15a15 15 0 0 1-15-15v-90c0-8.25 4.77-19.769 10.61-25.609l98.78-98.7805c5.83-5.83 15.38-5.83 21.21 0l98.79 98.7895c5.83 5.83 10.61 17.36 10.61 25.61v90-.01Z"/>
<path fill="#F2F4F9" d="m107.27 239.762-40.63-40.63c-2.09.72-4.32 1.13-6.64 1.13-11.3 0-20.5-9.2-20.5-20.5s9.2-20.5 20.5-20.5 20.5 9.2 20.5 20.5c0 2.33-.41 4.56-1.13 6.65l31.63 31.63v-115.88c-6.8-3.3395-11.5-10.3195-11.5-18.3895 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5c0 8.07-4.7 15.05-11.5 18.3895v81.27l31.46-31.46c-.62-1.96-.96-4.04-.96-6.2 0-11.3 9.2-20.5 20.5-20.5s20.5 9.2 20.5 20.5-9.2 20.5-20.5 20.5c-2.5 0-4.88-.47-7.09-1.29L129 208.892v30.88z"/>
</svg>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer-bottom"></div>
<div class="ohf-logo">
a project from
<img src="/static/icons/ohf.svg" alt="Open Home Foundation" height="32">
<img src="/static/images/ohf-badge.svg" alt="Home Assistant is a project by the Open Home Foundation" height="46">
</div>
</div>
<home-assistant></home-assistant>

View File

@ -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"),

View File

@ -53,7 +53,7 @@ export class HaServiceAction extends LitElement implements ActionElement {
);
public static get defaultConfig() {
return { service: "", data: {} };
return { action: "", data: {} };
}
protected willUpdate(changedProperties: PropertyValues) {

View File

@ -353,6 +353,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
},
actions: {
title: "",
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
type: "icon-button",
showNarrow: true,
moveable: false,

View File

@ -217,6 +217,7 @@ export class HaWebhookTrigger extends LitElement {
ha-textfield > ha-icon-button {
--mdc-icon-button-size: 24px;
--mdc-icon-size: 18px;
color: var(--secondary-text-color);
}
ha-button-menu {

View File

@ -198,6 +198,7 @@ class HaBlueprintOverview extends LitElement {
},
actions: {
title: "",
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
type: "overflow-menu",
showNarrow: true,
moveable: false,

View File

@ -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);

View File

@ -123,13 +123,10 @@ export class DialogHelperDetail extends LitElement {
this._opened = true;
await this.updateComplete;
this.hass.loadFragmentTranslation("config");
Promise.all([
getConfigFlowHandlers(this.hass, ["helper"]),
// Ensure the titles are loaded before we render the flows.
this.hass.loadBackendTranslation("title", undefined, true),
]).then(([flows]) => {
this._helperFlows = flows;
});
const flows = await getConfigFlowHandlers(this.hass, ["helper"]);
await this.hass.loadBackendTranslation("title", flows, true);
// Ensure the titles are loaded before we render the flows.
this._helperFlows = flows;
}
public closeDialog(): void {

View File

@ -349,7 +349,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
},
actions: {
title: "",
label: "Actions",
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
type: "overflow-menu",
hideable: false,
moveable: false,

View File

@ -13,6 +13,7 @@ import {
mdiDevices,
mdiDotsVertical,
mdiDownload,
mdiFileCodeOutline,
mdiHandExtendedOutline,
mdiOpenInNew,
mdiPackageVariant,
@ -329,6 +330,22 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
)}</ha-alert
>`
: ""}
${normalEntries.length === 0 &&
this._manifest &&
!this._manifest.config_flow &&
this.hass.config.components.find(
(comp) => comp.split(".")[0] === this.domain
)
? html`<ha-alert alert-type="info"
><ha-svg-icon
slot="icon"
path=${mdiFileCodeOutline}
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.integrations.config_entry.no_config_flow"
)}</ha-alert
>`
: ""}
</div>
<div class="card-actions">

View File

@ -1,5 +1,5 @@
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
import { mdiCloud, mdiCodeBraces, mdiPackageVariant } from "@mdi/js";
import { mdiCloud, mdiFileCodeOutline, mdiPackageVariant } from "@mdi/js";
import {
CSSResultGroup,
LitElement,
@ -184,7 +184,7 @@ export class HaIntegrationCard extends LitElement {
: nothing}
${this.manifest && !this.manifest?.config_flow
? html`<div class="icon yaml">
<ha-svg-icon .path=${mdiCodeBraces}></ha-svg-icon>
<ha-svg-icon .path=${mdiFileCodeOutline}></ha-svg-icon>
<simple-tooltip
animation-delay="0"
.position=${computeRTL(this.hass) ? "right" : "left"}

View File

@ -311,6 +311,9 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
},
only_editable: {
title: "",
label: this.hass.localize(
"ui.panel.config.scene.picker.headers.editable"
),
type: "icon",
showNarrow: true,
template: (scene) =>
@ -330,6 +333,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
},
actions: {
title: "",
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
type: "overflow-menu",
showNarrow: true,
moveable: false,

View File

@ -488,7 +488,9 @@ export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
if (value && !Array.isArray(value)) {
config.sequence = [value];
}
config.sequence = migrateAutomationAction(config.sequence);
if (config.sequence) {
config.sequence = migrateAutomationAction(config.sequence);
}
return config;
}

View File

@ -321,6 +321,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
},
actions: {
title: "",
label: this.hass.localize("ui.panel.config.generic.headers.actions"),
type: "overflow-menu",
showNarrow: true,
moveable: false,

View File

@ -38,7 +38,7 @@ export class AssistPipelineEvents extends LitElement {
</ha-card>`;
}
return html`<ha-alert alert-type="warning"
>There where no events in this run.</ha-alert
>There were no events in this run.</ha-alert
>`;
}
return html`

View File

@ -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"),

View File

@ -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;

View File

@ -57,9 +57,6 @@ export class HuiBadge extends ReactiveElement {
}
private _updateElement(config: LovelaceBadgeConfig) {
if (config.type === "state-label") {
config = { ...config, type: "entity" };
}
if (!this._element) {
return;
}
@ -69,9 +66,6 @@ export class HuiBadge extends ReactiveElement {
}
private _loadElement(config: LovelaceBadgeConfig) {
if (config.type === "state-label") {
config = { ...config, type: "entity" };
}
this._element = createBadgeElement(config);
this._elementConfig = config;
if (this.hass) {

View File

@ -5,6 +5,7 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { mdiAlertCircle } from "@mdi/js";
import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { computeDomain } from "../../../common/entity/compute_domain";
@ -12,6 +13,7 @@ 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 { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
@ -129,7 +131,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="content">
<span class="name">${entityId}</span>
<span class="state">
${this.hass.localize("ui.badge.entity.not_found")}
</span>
</span>
</div>
`;
}
const active = stateActive(stateObj);
@ -144,6 +156,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.name=${this._config.name}
>
</state-display>
`;
@ -205,6 +218,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);
@ -224,8 +240,14 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge {
box-sizing: border-box;
width: auto;
border-radius: 18px;
background-color: var(--card-background-color, white);
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,
@ -276,7 +298,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;
}

View File

@ -1,71 +1,25 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { customElement } from "lit/decorators";
import "../../../components/entity/ha-state-label-badge";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { LovelaceBadge } from "../types";
import { StateLabelBadgeConfig } from "./types";
import { HuiStateLabelBadgeEditor } from "../editor/config-elements/hui-state-label-badge-editor";
import { HuiEntityBadge } from "./hui-entity-badge";
import { EntityBadgeConfig, StateLabelBadgeConfig } from "./types";
@customElement("hui-state-label-badge")
export class HuiStateLabelBadge extends LitElement implements LovelaceBadge {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() protected _config?: StateLabelBadgeConfig;
public setConfig(config: StateLabelBadgeConfig): void {
this._config = config;
export class HuiStateLabelBadge extends HuiEntityBadge {
public static async getConfigElement(): Promise<HuiStateLabelBadgeEditor> {
await import("../editor/config-elements/hui-state-label-badge-editor");
return document.createElement("hui-state-label-badge-editor");
}
protected render() {
if (!this._config || !this.hass) {
return nothing;
}
// @ts-ignore
public override setConfig(config: StateLabelBadgeConfig): void {
const entityBadgeConfig: EntityBadgeConfig = {
type: "entity",
entity: config.entity,
display_type: config.show_name === false ? "standard" : "complete",
};
const stateObj = this.hass.states[this._config.entity!];
return html`
<ha-state-label-badge
.hass=${this.hass}
.state=${stateObj}
.name=${this._config.name}
.icon=${this._config.icon}
.image=${this._config.image}
.showName=${this._config.show_name ?? true}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
tabindex=${ifDefined(
hasAction(this._config.tap_action) || this._config.entity
? "0"
: undefined
)}
></ha-state-label-badge>
`;
}
private _handleAction(ev: ActionHandlerEvent) {
handleAction(this, this.hass!, this._config!, ev.detail.action!);
}
static get styles(): CSSResultGroup {
return css`
ha-state-label-badge:focus {
outline: none;
background: var(--divider-color);
border-radius: 4px;
}
ha-state-label-badge {
display: inline-block;
padding: 4px 2px 4px 2px;
margin: -4px -2px -4px -2px;
}
`;
this._config = entityBadgeConfig;
}
}

View File

@ -150,10 +150,10 @@ export class HuiViewBadges extends LitElement {
class="add"
@click=${this._addBadge}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.add_card"
"ui.panel.lovelace.editor.section.add_badge"
)}
.title=${this.hass.localize(
"ui.panel.lovelace.editor.section.add_card"
"ui.panel.lovelace.editor.section.add_badge"
)}
>
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>

View File

@ -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 {

View File

@ -23,7 +23,7 @@ import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasAction, hasAnyAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
@ -128,24 +128,20 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
const name = this._config.name ?? computeStateName(stateObj);
const hasAnyAction =
!this._config.tap_action ||
hasAction(this._config.tap_action) ||
hasAction(this._config.hold_action) ||
hasAction(this._config.double_tap_action);
// Use `stateObj.state` as value to keep formatting (e.g trailing zeros)
// for consistent value display across gauge, entity, entity-row, etc.
return html`
<ha-card
class=${classMap({ action: hasAnyAction })}
class=${classMap({ action: hasAnyAction(this._config) })}
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this._config.hold_action),
hasDoubleClick: hasAction(this._config.double_tap_action),
})}
tabindex=${ifDefined(
hasAction(this._config.tap_action) ? "0" : undefined
!this._config.tap_action || hasAction(this._config.tap_action)
? "0"
: undefined
)}
>
<ha-gauge

View File

@ -28,7 +28,7 @@ import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasAction, hasAnyAction } from "../common/has-action";
import { hasConfigOrEntitiesChanged } from "../common/has-changed";
import { processConfigEntities } from "../common/process-config-entities";
import "../components/hui-timestamp-display";
@ -263,15 +263,9 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
const name = entityConf.name ?? computeStateName(stateObj);
const hasAnyAction =
!entityConf.tap_action ||
hasAction(entityConf.tap_action) ||
hasAction(entityConf.hold_action) ||
hasAction(entityConf.double_tap_action);
return html`
<div
class=${classMap({ entity: true, action: hasAnyAction })}
class=${classMap({ entity: true, action: hasAnyAction(entityConf) })}
.config=${entityConf}
@action=${this._handleAction}
.actionHandler=${actionHandler({

View File

@ -251,6 +251,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.name=${this._config.name}
>
</state-display>
`;

View File

@ -430,6 +430,14 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
this._config?.show_current !== false &&
this._config?.show_forecast !== false
) {
return {
grid_columns: 4,
grid_min_columns: 2,
grid_rows: 4,
grid_min_rows: 4,
};
}
if (this._config?.show_forecast !== false) {
return {
grid_columns: 4,
grid_min_columns: 2,
@ -441,7 +449,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
grid_columns: 4,
grid_min_columns: 2,
grid_rows: 2,
grid_min_rows: 1,
grid_min_rows: 2,
};
}

View File

@ -12,7 +12,7 @@ export function createStyledHuiElement(
if (elementConfig.style) {
Object.keys(elementConfig.style).forEach((prop) => {
element.style.setProperty(prop, elementConfig.style[prop]);
element.style.setProperty(prop, elementConfig.style![prop]);
});
}

View File

@ -44,6 +44,7 @@ const HIDE_DOMAIN = new Set([
"persistent_notification",
"script",
"sun",
"tag",
"todo",
"zone",
...ASSIST_ENTITIES,

View File

@ -1,5 +1,15 @@
import { ActionConfig } from "../../../data/lovelace/config/action";
import { ConfigEntity } from "../cards/types";
export function hasAction(config?: ActionConfig): boolean {
return config !== undefined && config.action !== "none";
}
export function hasAnyAction(config: ConfigEntity): boolean {
return (
!config.tap_action ||
hasAction(config.tap_action) ||
hasAction(config.hold_action) ||
hasAction(config.double_tap_action)
);
}

View File

@ -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,7 +299,8 @@ export class HuiActionEditor extends LitElement {
ev.stopPropagation();
const value = {
...this.config!,
perform_action: ev.detail.value.service || "",
action: "perform-action",
perform_action: ev.detail.value.action || "",
data: ev.detail.value.data,
target: ev.detail.value.target || {},
};

View File

@ -20,7 +20,7 @@ import { HomeAssistant } from "../../../types";
import { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasAction, hasAnyAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "./hui-warning";
@customElement("hui-generic-entity-row")
@ -60,9 +60,7 @@ export class HuiGenericEntityRow extends LitElement {
// By default, we always show a pointer, since if there is no explicit configuration provided,
// the frontend always assumes "more-info" in the action handler. We only need to hide the pointer
// if the tap action is explicitly set to "none".
const pointer = !(
this.config.tap_action && this.config.tap_action.action === "none"
);
const pointer = hasAnyAction(this.config);
const hasSecondary = this.secondaryText || this.config.secondary_info;
const name = this.config.name ?? computeStateName(stateObj);
@ -82,7 +80,11 @@ export class HuiGenericEntityRow extends LitElement {
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
tabindex=${ifDefined(pointer ? "0" : undefined)}
tabindex=${ifDefined(
!this.config.tap_action || hasAction(this.config.tap_action)
? "0"
: undefined
)}
></state-badge>
${!this.hideName
? html`<div

View File

@ -35,7 +35,7 @@ export const createPictureElementElement = (config: LovelaceElementConfig) =>
export const getPictureElementClass = (type: string) =>
getLovelaceElementClass(
type,
type === "action-button" ? "service-button" : type,
"element",
ALWAYS_LOADED_TYPES,
LAZY_LOAD_TYPES

View File

@ -1,3 +1,5 @@
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import { css, CSSResultGroup, html, nothing, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";

View File

@ -109,10 +109,6 @@ export class HuiDialogEditBadge
this._badgeConfig = badge != null ? ensureBadgeConfig(badge) : badge;
}
if (this._badgeConfig?.type === "state-label") {
this._badgeConfig = { ...this._badgeConfig, type: "entity" };
}
this.large = false;
if (this._badgeConfig && !Object.isFrozen(this._badgeConfig)) {
this._badgeConfig = deepFreeze(this._badgeConfig);

View File

@ -1,5 +1,5 @@
import "@polymer/paper-tabs/paper-tab";
import "@polymer/paper-tabs/paper-tabs";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { CSSResultGroup, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { LovelaceCardConfig } from "../../../../data/lovelace/config/card";

View File

@ -235,9 +235,11 @@ export class HaCardConditionEditor extends LitElement {
? this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.testing_pass"
)
: this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.testing_error"
)
: this._testingResult === false
? this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.testing_error"
)
: nothing
}
</div>
</ha-card>

View File

@ -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,
};

View File

@ -106,12 +106,23 @@ export class HuiConditionalElementEditor
private _elementsChanged(ev: CustomEvent): void {
ev.stopPropagation();
const oldLength = this._config?.elements?.length || 0;
const config = {
...this._config,
elements: ev.detail.elements as LovelaceElementConfig[],
} as LovelaceCardConfig;
fireEvent(this, "config-changed", { config });
const newLength = ev.detail.elements?.length || 0;
if (newLength === oldLength + 1) {
const index = newLength - 1;
this._subElementEditorConfig = {
index,
type: "element",
elementConfig: { ...ev.detail.elements[index] },
};
}
}
private _handleSubElementChanged(ev: CustomEvent): void {

View File

@ -122,7 +122,9 @@ export class HuiEntityBadgeEditor
{
name: "state_content",
selector: {
ui_state_content: {},
ui_state_content: {
allow_name: true,
},
},
context: {
filter_entity: "entity",

View File

@ -23,7 +23,7 @@ import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { DEFAULT_MIN, DEFAULT_MAX } from "../../cards/hui-gauge-card";
import { UiAction } from "../../components/hui-action-editor";
const TAP_ACTIONS: UiAction[] = ["navigate", "url", "call-service", "none"];
const TAP_ACTIONS: UiAction[] = ["navigate", "url", "perform-action", "none"];
const gaugeSegmentStruct = object({
from: number(),

View File

@ -138,12 +138,23 @@ export class HuiPictureElementsCardEditor
private _elementsChanged(ev: CustomEvent): void {
ev.stopPropagation();
const oldLength = this._config?.elements?.length || 0;
const config = {
...this._config,
elements: ev.detail.elements as LovelaceElementConfig[],
} as LovelaceCardConfig;
fireEvent(this, "config-changed", { config });
const newLength = ev.detail.elements?.length || 0;
if (newLength === oldLength + 1) {
const index = newLength - 1;
this._subElementEditorConfig = {
index,
type: "element",
elementConfig: { ...ev.detail.elements[index] },
};
}
}
private _handleSubElementChanged(ev: CustomEvent): void {

View File

@ -0,0 +1,45 @@
import { customElement } from "lit/decorators";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import "../../../../components/ha-form/ha-form";
import { EntityBadgeConfig } from "../../badges/types";
import "../hui-sub-element-editor";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct";
import "./hui-card-features-editor";
import { HuiEntityBadgeEditor } from "./hui-entity-badge-editor";
const badgeConfigStruct = assign(
baseLovelaceBadgeConfig,
object({
entity: optional(string()),
name: optional(string()),
icon: optional(string()),
show_entity_picture: optional(boolean()),
tap_action: optional(actionConfigStruct),
show_name: optional(boolean()),
image: optional(string()),
})
);
@customElement("hui-state-label-badge-editor")
export class HuiStateLabelBadgeEditor extends HuiEntityBadgeEditor {
// @ts-ignore
public override setConfig(config: StateLabelBadgeConfig): void {
assert(config, badgeConfigStruct);
const entityBadgeConfig: EntityBadgeConfig = {
type: "entity",
entity: config.entity,
display_type: config.show_name === false ? "standard" : "complete",
};
// @ts-ignore
this._config = entityBadgeConfig;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-state-label-badge-editor": HuiStateLabelBadgeEditor;
}
}

View File

@ -0,0 +1,30 @@
import { LovelaceElementConfig } from "../elements/types";
import { HomeAssistant } from "../../../types";
import { getPictureElementClass } from "../create-element/create-picture-element";
export const getElementStubConfig = async (
hass: HomeAssistant,
type: string,
entities: string[],
entitiesFallback: string[]
): Promise<LovelaceElementConfig> => {
let elementConfig: LovelaceElementConfig = { type };
if (type !== "conditional") {
elementConfig.style = { left: "50%", top: "50%" };
}
const elClass = await getPictureElementClass(type);
if (elClass && elClass.getStubConfig) {
const classStubConfig = await elClass.getStubConfig(
hass,
entities,
entitiesFallback
);
elementConfig = { ...elementConfig, ...classStubConfig };
}
return elementConfig;
};

View File

@ -1,3 +1,4 @@
import deepClone from "deep-clone-simple";
import { mdiClose, mdiPencil, mdiContentDuplicate } from "@mdi/js";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
@ -9,6 +10,7 @@ import { HomeAssistant } from "../../../types";
import "../../../components/ha-select";
import type { HaSelect } from "../../../components/ha-select";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { getElementStubConfig } from "./get-element-stub-config";
import {
ConditionalElementConfig,
IconElementConfig,
@ -171,17 +173,14 @@ export class HuiPictureElementsCardRowEditor extends LitElement {
if (value === "") {
return;
}
const newElements = this.elements!.concat({
type: value! as string,
...(value !== "conditional"
? {
style: {
top: "50%",
left: "50%",
},
}
: {}),
} as LovelaceElementConfig);
const newElements = this.elements!.concat(
await getElementStubConfig(
this.hass!,
value,
Object.keys(this.hass!.entities),
[]
)
);
fireEvent(this, "elements-changed", { elements: newElements });
this._select.select(-1);
}
@ -225,7 +224,7 @@ export class HuiPictureElementsCardRowEditor extends LitElement {
private _duplicateRow(ev: CustomEvent): void {
const index = (ev.currentTarget as any).index;
const newElements = [...this.elements!, this.elements![index]];
const newElements = [...this.elements!, deepClone(this.elements![index])];
fireEvent(this, "elements-changed", { elements: newElements });
}

View File

@ -53,9 +53,19 @@ export class HuiSubElementEditor extends LitElement {
@click=${this._goBack}
></ha-icon-button-prev>
<span slot="title"
>${this.hass.localize(
`ui.panel.lovelace.editor.sub-element-editor.types.${this.config?.type}`
)}</span
>${this.config?.type === "element"
? this.hass.localize(
`ui.panel.lovelace.editor.sub-element-editor.types.element_type`,
{
type:
this.hass.localize(
`ui.panel.lovelace.editor.card.picture-elements.element_types.${this.config?.elementConfig?.type}`
) || this.config?.elementConfig?.type,
}
)
: this.hass.localize(
`ui.panel.lovelace.editor.sub-element-editor.types.${this.config?.type}`
)}</span
>
</div>
<ha-icon-button

View File

@ -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),

View File

@ -18,6 +18,10 @@ export class HuiIconElement extends LitElement implements LovelaceElement {
return document.createElement("hui-icon-element-editor");
}
public static getStubConfig(): IconElementConfig {
return { type: "icon", icon: "mdi:alert-circle" };
}
public hass?: HomeAssistant;
@state() private _config?: IconElementConfig;

View File

@ -17,6 +17,14 @@ export class HuiServiceButtonElement
return document.createElement("hui-service-button-element-editor");
}
public static getStubConfig(hass: HomeAssistant): ServiceButtonElementConfig {
return {
type: "action-button",
action: "homeassistant.turn_on",
title: hass.localize("ui.card.common.turn_on"),
};
}
public hass?: HomeAssistant;
@state() private _config?: ServiceButtonElementConfig;

View File

@ -1,6 +1,8 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { LitElement, PropertyValues, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { findEntities } from "../common/find-entities";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/entity/ha-state-label-badge";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@ -8,6 +10,7 @@ import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { isUnavailableState } from "../../../data/entity";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-warning-element";
@ -26,6 +29,27 @@ export class HuiStateBadgeElement
return document.createElement("hui-state-badge-element-editor");
}
public static getStubConfig(
hass: HomeAssistant,
entities: string[],
entitiesFallback: string[]
): StateBadgeElementConfig {
const includeDomains = ["light", "switch", "sensor"];
const maxEntities = 1;
const entityFilter = (stateObj: HassEntity): boolean =>
!isUnavailableState(stateObj.state);
const foundEntities = findEntities(
hass,
maxEntities,
entities,
entitiesFallback,
includeDomains,
entityFilter
);
return { type: "state-badge", entity: foundEntities[0] || "" };
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateBadgeElementConfig;

View File

@ -1,3 +1,4 @@
import type { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
@ -8,12 +9,14 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { findEntities } from "../common/find-entities";
import "../../../components/entity/state-badge";
import { HomeAssistant } from "../../../types";
import { computeTooltip } from "../common/compute-tooltip";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { isUnavailableState } from "../../../data/entity";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-warning-element";
@ -30,6 +33,27 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
return document.createElement("hui-state-icon-element-editor");
}
public static getStubConfig(
hass: HomeAssistant,
entities: string[],
entitiesFallback: string[]
): StateIconElementConfig {
const includeDomains = ["light", "switch", "sensor"];
const maxEntities = 1;
const entityFilter = (stateObj: HassEntity): boolean =>
!isUnavailableState(stateObj.state);
const foundEntities = findEntities(
hass,
maxEntities,
entities,
entitiesFallback,
includeDomains,
entityFilter
);
return { type: "state-icon", entity: foundEntities[0] || "" };
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateIconElementConfig;

View File

@ -1,3 +1,4 @@
import type { HassEntity } from "home-assistant-js-websocket";
import {
CSSResultGroup,
LitElement,
@ -8,12 +9,14 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { findEntities } from "../common/find-entities";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import { HomeAssistant } from "../../../types";
import { computeTooltip } from "../common/compute-tooltip";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { isUnavailableState } from "../../../data/entity";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-warning-element";
@ -29,6 +32,27 @@ class HuiStateLabelElement extends LitElement implements LovelaceElement {
return document.createElement("hui-state-label-element-editor");
}
public static getStubConfig(
hass: HomeAssistant,
entities: string[],
entitiesFallback: string[]
): StateLabelElementConfig {
const includeDomains = ["light", "switch", "sensor"];
const maxEntities = 1;
const entityFilter = (stateObj: HassEntity): boolean =>
!isUnavailableState(stateObj.state);
const foundEntities = findEntities(
hass,
maxEntities,
entities,
entitiesFallback,
includeDomains,
entityFilter
);
return { type: "state-label", entity: foundEntities[0] || "" };
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: StateLabelElementConfig;

View File

@ -6,7 +6,7 @@ import { HuiImage } from "../components/hui-image";
interface LovelaceElementConfigBase {
type: string;
style: Record<string, string>;
style?: Record<string, string>;
}
export type LovelaceElementConfig =

View File

@ -123,7 +123,6 @@ class HuiNumberEntityRow extends LitElement implements LovelaceRow {
static get styles(): CSSResultGroup {
return css`
:host {
cursor: pointer;
display: block;
}
.flex {

View File

@ -70,9 +70,6 @@ class HuiSceneEntityRow extends LitElement implements LovelaceRow {
margin-inline-end: -0.57em;
margin-inline-start: initial;
}
:host {
cursor: pointer;
}
`;
}

View File

@ -58,9 +58,6 @@ class HuiSimpleEntityRow extends LitElement implements LovelaceRow {
div {
text-align: right;
}
.pointer {
cursor: pointer;
}
`;
}
}

View File

@ -61,9 +61,6 @@ class HuiUpdateEntityRow extends LitElement implements LovelaceRow {
div {
text-align: right;
}
.pointer {
cursor: pointer;
}
`;
}
}

View File

@ -26,7 +26,7 @@ import type { HomeAssistant } from "../../../types";
import type { EntitiesCardEntityConfig } from "../cards/types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { hasAction, hasAnyAction } from "../common/has-action";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import { createEntityNotFoundWarning } from "../components/hui-warning";
@ -118,9 +118,7 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
`;
}
const pointer = !(
this._config.tap_action && this._config.tap_action.action !== "none"
);
const pointer = hasAnyAction(this._config);
const hasSecondary = this._config.secondary_info;
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
@ -138,7 +136,11 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
hasHold: hasAction(this._config!.hold_action),
hasDoubleClick: hasAction(this._config!.double_tap_action),
})}
tabindex=${ifDefined(pointer ? "0" : undefined)}
tabindex=${ifDefined(
!this._config.tap_action || hasAction(this._config.tap_action)
? "0"
: undefined
)}
>
${weatherStateIcon ||
html`

View File

@ -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;
}

View File

@ -109,6 +109,11 @@ export interface LovelaceRowConstructor extends Constructor<LovelaceRow> {
export interface LovelaceElementConstructor
extends Constructor<LovelaceElement> {
getConfigElement?: () => LovelacePictureElementEditor;
getStubConfig?: (
hass: HomeAssistant,
entities: string[],
entitiesFallback: string[]
) => LovelaceElementConfig;
}
export interface LovelaceHeaderFooter extends HTMLElement {

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
};
}
}

View File

@ -222,6 +222,7 @@ class HaProfileSectionGeneral extends LitElement {
text: this.hass.localize("ui.panel.profile.logout_text"),
confirmText: this.hass.localize("ui.panel.profile.logout"),
confirm: () => fireEvent(this, "hass-logout"),
destructive: true,
});
}

View File

@ -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

View File

@ -55,6 +55,8 @@ class StateDisplay extends LitElement {
@property({ attribute: false }) public content?: StateContent;
@property({ attribute: false }) public name?: string;
protected createRenderRoot() {
return this;
}
@ -88,6 +90,9 @@ class StateDisplay extends LitElement {
return this.hass!.formatEntityState(stateObj);
}
if (content === "name") {
return html`${this.name || stateObj.attributes.friendly_name}`;
}
// Check last-changed for backwards compatibility
if (content === "last_changed" || content === "last-changed") {
return html`

View File

@ -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"
}
},
@ -1026,6 +1031,7 @@
},
"state-content-picker": {
"state": "State",
"name": "Name",
"last_changed": "Last changed",
"last_updated": "Last updated",
"remaining_time": "Remaining time",
@ -3743,6 +3749,7 @@
"name": "Name",
"last_activated": "Last activated",
"category": "Category",
"editable": "[%key:ui::panel::config::helpers::picker::headers::editable%]",
"area": "Area",
"icon": "Icon"
},
@ -5490,8 +5497,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}",
@ -5625,6 +5632,7 @@
},
"section": {
"unnamed_section": "Unnamed section",
"add_badge": "Add badge",
"add_card": "[%key:ui::panel::lovelace::editor::edit_card::add%]",
"create_section": "Create section"
},
@ -6323,7 +6331,8 @@
"footer": "Footer editor",
"row": "Entity row editor",
"feature": "Feature editor",
"element": "Element editor"
"element": "Element editor",
"element_type": "{type} element editor"
}
}
},
@ -6351,12 +6360,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"
@ -6853,6 +6856,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?",

1239
yarn.lock

File diff suppressed because it is too large Load Diff