Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen
3b69f9cc8d Update Z-Wave data type
Data added in https://github.com/home-assistant/core/pull/117288/files
2024-05-12 22:24:11 -04:00
882 changed files with 18083 additions and 38380 deletions

View File

@@ -1,25 +1,28 @@
[modern]
# Modern builds target recent browsers supporting the latest features to minimize transpilation, polyfills, etc.
# It is served to browsers meeting the following requirements:
# - released in the last year + current alpha/beta versions
# - Firefox extended support release (ESR)
# - with global utilization at or above 0.5%
# - must support dynamic import of ES modules
# - exclude browsers no longer being maintained
# - exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
unreleased versions
last 1 year
Firefox ESR
>= 0.5% and supports es6-module-dynamic-import
not dead
# Support for dynamic import is the main litmus test for serving modern builds.
# Although officially a ES2020 feature, browsers implemented it early, so this
# enables all of ES2017 and some features in ES2018.
supports es6-module-dynamic-import
# Exclude Safari 11-12 because of a bug in tagged template literals
# https://bugs.webkit.org/show_bug.cgi?id=190756
# Note: Dropping version 11 also enables several more ES2018 features
not Safari < 13
not iOS < 13
# Exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
# Babel ignores these automatically, but we need here for Webpack to output ESM with dynamic imports
not KaiOS > 0
not QQAndroid > 0
not UCAndroid > 0
# Exclude unsupported browsers
not dead
[legacy]
# Legacy builds are served when modern requirements are not met and support browsers:
# - released in the last 7 years + current alpha/beta versionss
# - with global utilization at or above 0.05%
# - with global utilization above 0.05%
# The lattermost query ensures that support for popular old browsers is not dropped too early
# (e.g. IE 11, Android 4.4, or Samsung 4).
#
@@ -33,10 +36,4 @@ not UCAndroid > 0
# As of May 2023, only web sockets must be added to the query.
unreleased versions
last 7 years
>= 0.05% and supports websockets
[legacy-sw]
# Same as legacy plus supports service workers
unreleased versions
last 7 years
>= 0.05% and supports websockets and supports serviceworkers
> 0.05% and supports websockets

View File

@@ -8,7 +8,6 @@
"postCreateCommand": "sudo apt update && sudo apt upgrade -y && sudo apt install -y libpcap-dev",
"postStartCommand": "script/bootstrap",
"containerEnv": {
"DEV_CONTAINER": "1",
"WORKSPACE_DIRECTORY": "${containerWorkspaceFolder}"
},
"customizations": {

View File

@@ -115,7 +115,6 @@
}
],
"unused-imports/no-unused-imports": "error",
"lit/attribute-names": "warn",
"lit/attribute-value-entities": "off",
"lit/no-template-map": "off",
"lit/no-native-attributes": "warn",
@@ -126,5 +125,6 @@
"lit-a11y/anchor-is-valid": "warn",
"lit-a11y/role-has-required-aria-attrs": "warn"
},
"plugins": ["unused-imports"]
"plugins": ["disable", "unused-imports"],
"processor": "disable/disable"
}

View File

@@ -21,12 +21,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,12 +57,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -24,9 +24,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -37,7 +37,7 @@ jobs:
- name: Build resources
run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
- name: Setup lint cache
uses: actions/cache@v4.1.2
uses: actions/cache@v4.0.2
with:
path: |
node_modules/.cache/prettier
@@ -58,9 +58,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -76,9 +76,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -89,7 +89,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.3.3
with:
name: frontend-bundle-stats
path: build/stats/*.json
@@ -100,9 +100,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -113,7 +113,7 @@ jobs:
env:
IS_TEST: "true"
- name: Upload bundle stats
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.3.3
with:
name: supervisor-bundle-stats
path: build/stats/*.json

View File

@@ -23,7 +23,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.

View File

@@ -22,12 +22,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
with:
ref: dev
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -58,12 +58,12 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
with:
ref: master
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -16,10 +16,10 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -21,10 +21,10 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps:
- name: Check out files from GitHub
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn

View File

@@ -20,7 +20,7 @@ jobs:
contents: write
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v5
@@ -28,7 +28,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -57,14 +57,14 @@ jobs:
run: tar -czvf translations.tar.gz translations
- name: Upload build artifacts
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.3.3
with:
name: wheels
path: dist/home_assistant_frontend*.whl
if-no-files-found: error
- name: Upload translations
uses: actions/upload-artifact@v4.4.3
uses: actions/upload-artifact@v4.3.3
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.12
uses: relative-ci/agent-action@v2.1.10
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

View File

@@ -23,7 +23,7 @@ jobs:
contents: write # Required to upload release assets
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@master
@@ -34,7 +34,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Setup Node
uses: actions/setup-node@v4.1.0
uses: actions/setup-node@v4.0.2
with:
node-version-file: ".nvmrc"
cache: yarn
@@ -55,7 +55,7 @@ jobs:
script/release
- name: Upload release assets
uses: softprops/action-gh-release@v2.0.8
uses: softprops/action-gh-release@v2.0.4
with:
files: |
dist/*.whl
@@ -74,9 +74,9 @@ jobs:
echo "home-assistant-frontend==$version" > ./requirements.txt
- name: Build wheels
uses: home-assistant/wheels@2024.07.1
uses: home-assistant/wheels@2024.01.0
with:
abi: cp312
abi: cp311
tag: musllinux_1_2
arch: amd64
wheels-key: ${{ secrets.WHEELS_KEY }}

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repository
uses: actions/checkout@v4.2.2
uses: actions/checkout@v4.1.4
- name: Upload Translations
run: |

View File

@@ -1 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn run lint-staged --relative --shell "/bin/bash"

View File

@@ -1,7 +1,16 @@
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
--- a/modular/sortable.core.esm.js
+++ b/modular/sortable.core.esm.js
@@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
}
target = parent; // store last element
}
- /* jshint boss:true */ while (parent = parent.parentNode);
+ /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
}
_unhideGhostForTarget();
}
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
}
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
@@ -24,7 +33,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
}
parentEl = el; // actualization
@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
@@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
targetRect = getRect(target);
if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
capture();
@@ -35,10 +44,11 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
+ catch(err) {
+ return completed(false);
+ }
+
parentEl = el; // actualization
changed();
@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
@@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
_silent = true;
setTimeout(_unsilent, 30);
capture();
@@ -46,6 +56,8 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
- el.appendChild(dragEl);
- } else {
- target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
- }
+ try {
+ if (after && !nextSibling) {
+ el.appendChild(dragEl);
@@ -55,6 +67,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5
+ }
+ catch(err) {
+ return completed(false);
}
+ }
// Undo chrome's scroll adjustment (has no effect on other browsers)
if (scrolledPastTop) {
scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);

View File

@@ -1,55 +0,0 @@
diff --git a/build/inject-manifest.js b/build/inject-manifest.js
index 60e3d2bb51c11a19fbbedbad65e101082ec41c36..fed6026630f43f86e25446383982cf6fb694313b 100644
--- a/build/inject-manifest.js
+++ b/build/inject-manifest.js
@@ -104,7 +104,7 @@ async function injectManifest(config) {
replaceString: manifestString,
searchString: options.injectionPoint,
});
- filesToWrite[options.swDest] = source;
+ filesToWrite[options.swDest] = source.replace(url, encodeURI(upath_1.default.basename(destPath)));
filesToWrite[destPath] = map;
}
else {
diff --git a/build/lib/translate-url-to-sourcemap-paths.js b/build/lib/translate-url-to-sourcemap-paths.js
index 3220c5474eeac6e8a56ca9b2ac2bd9be48529e43..5f003879a904d4840529a42dd056d288fd213771 100644
--- a/build/lib/translate-url-to-sourcemap-paths.js
+++ b/build/lib/translate-url-to-sourcemap-paths.js
@@ -22,7 +22,7 @@ function translateURLToSourcemapPaths(url, swSrc, swDest) {
const possibleSrcPath = upath_1.default.resolve(upath_1.default.dirname(swSrc), url);
if (fs_extra_1.default.existsSync(possibleSrcPath)) {
srcPath = possibleSrcPath;
- destPath = upath_1.default.resolve(upath_1.default.dirname(swDest), url);
+ destPath = `${swDest}.map`;
}
else {
warning = `${errors_1.errors['cant-find-sourcemap']} ${possibleSrcPath}`;
diff --git a/src/inject-manifest.ts b/src/inject-manifest.ts
index 8795ddcaa77aea7b0356417e4bc4b19e2b3f860c..fcdc68342d9ac53936c9ed40a9ccfc2f5070cad3 100644
--- a/src/inject-manifest.ts
+++ b/src/inject-manifest.ts
@@ -129,7 +129,10 @@ export async function injectManifest(
searchString: options.injectionPoint!,
});
- filesToWrite[options.swDest] = source;
+ filesToWrite[options.swDest] = source.replace(
+ url!,
+ encodeURI(upath.basename(destPath)),
+ );
filesToWrite[destPath] = map;
} else {
// If there's no sourcemap associated with swSrc, a simple string
diff --git a/src/lib/translate-url-to-sourcemap-paths.ts b/src/lib/translate-url-to-sourcemap-paths.ts
index 072eac40d4ef5d095a01cb7f7e392a9e034853bd..f0bbe69e88ef3a415de18a7e9cb264daea273d71 100644
--- a/src/lib/translate-url-to-sourcemap-paths.ts
+++ b/src/lib/translate-url-to-sourcemap-paths.ts
@@ -28,7 +28,7 @@ export function translateURLToSourcemapPaths(
const possibleSrcPath = upath.resolve(upath.dirname(swSrc), url);
if (fse.existsSync(possibleSrcPath)) {
srcPath = possibleSrcPath;
- destPath = upath.resolve(upath.dirname(swDest), url);
+ destPath = `${swDest}.map`;
} else {
warning = `${errors['cant-find-sourcemap']} ${possibleSrcPath}`;
}

894
.yarn/releases/yarn-4.2.2.cjs vendored Executable file

File diff suppressed because one or more lines are too long

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.5.1.cjs
yarnPath: .yarn/releases/yarn-4.2.2.cjs

View File

@@ -27,5 +27,3 @@ A complete guide can be found at the following [link](https://www.home-assistant
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
[![Home Assistant - A project from the Open Home Foundation](https://www.openhomefoundation.org/badges/home-assistant.png)](https://www.openhomefoundation.org/)

View File

@@ -1,56 +1,7 @@
import defineProvider from "@babel/helper-define-polyfill-provider";
import { join } from "node:path";
import paths from "../paths.cjs";
const POLYFILL_DIR = join(paths.polymer_dir, "src/resources/polyfills");
// List of polyfill keys with supported browser targets for the functionality
const PolyfillSupport = {
// Note states and shadowRoot properties should be supported.
"element-internals": {
android: 90,
chrome: 90,
edge: 90,
firefox: 126,
ios: 17.4,
opera: 76,
opera_mobile: 64,
safari: 17.4,
samsung: 15.0,
},
"element-append": {
android: 54,
chrome: 54,
edge: 17,
firefox: 49,
ios: 10.0,
opera: 41,
opera_mobile: 41,
safari: 10.0,
samsung: 6.0,
},
"element-getattributenames": {
android: 61,
chrome: 61,
edge: 18,
firefox: 45,
ios: 10.3,
opera: 48,
opera_mobile: 45,
safari: 10.1,
samsung: 8.0,
},
"element-toggleattribute": {
android: 69,
chrome: 69,
edge: 18,
firefox: 63,
ios: 12.0,
opera: 56,
opera_mobile: 48,
safari: 12.0,
samsung: 10.0,
},
fetch: {
android: 42,
chrome: 42,
@@ -62,31 +13,6 @@ const PolyfillSupport = {
safari: 10.1,
samsung: 4.0,
},
"intl-getcanonicallocales": {
android: 54,
chrome: 54,
edge: 16,
firefox: 48,
ios: 10.3,
opera: 41,
opera_mobile: 41,
safari: 10.1,
samsung: 6.0,
},
"intl-locale": {
android: 74,
chrome: 74,
edge: 79,
firefox: 75,
ios: 14.0,
opera: 62,
opera_mobile: 53,
safari: 14.0,
samsung: 11.0,
},
"intl-other": {
// Not specified (i.e. always try polyfill) since compatibility depends on supported locales
},
proxy: {
android: 49,
chrome: 49,
@@ -98,67 +24,17 @@ const PolyfillSupport = {
safari: 10.0,
samsung: 5.0,
},
"resize-observer": {
android: 64,
chrome: 64,
edge: 79,
firefox: 69,
ios: 13.4,
opera: 51,
opera_mobile: 47,
safari: 13.1,
samsung: 9.0,
},
};
// Map of global variables and/or instance and static properties to the
// corresponding polyfill key and actual module to import
const polyfillMap = {
global: {
fetch: { key: "fetch", module: "unfetch/polyfill" },
Proxy: { key: "proxy", module: "proxy-polyfill" },
ResizeObserver: {
key: "resize-observer",
module: join(POLYFILL_DIR, "resize-observer.ts"),
},
},
instance: {
attachInternals: {
key: "element-internals",
module: "element-internals-polyfill",
},
...Object.fromEntries(
["append", "getAttributeNames", "toggleAttribute"].map((prop) => {
const key = `element-${prop.toLowerCase()}`;
return [prop, { key, module: join(POLYFILL_DIR, `${key}.ts`) }];
})
),
},
static: {
Intl: {
getCanonicalLocales: {
key: "intl-getcanonicallocales",
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
},
Locale: {
key: "intl-locale",
module: join(POLYFILL_DIR, "intl-polyfill.ts"),
},
...Object.fromEntries(
[
"DateTimeFormat",
"DisplayNames",
"ListFormat",
"NumberFormat",
"PluralRules",
"RelativeTimeFormat",
].map((obj) => [
obj,
{ key: "intl-other", module: join(POLYFILL_DIR, "intl-polyfill.ts") },
])
),
},
fetch: { key: "fetch", module: "unfetch/polyfill" },
},
instance: {},
static: {},
};
// Create plugin using the same factory as for CoreJS
@@ -166,16 +42,14 @@ export default defineProvider(
({ createMetaResolver, debug, shouldInjectPolyfill }) => {
const resolvePolyfill = createMetaResolver(polyfillMap);
return {
name: "custom-polyfill",
name: "HA Custom",
polyfills: PolyfillSupport,
usageGlobal(meta, utils) {
const polyfill = resolvePolyfill(meta);
if (polyfill && shouldInjectPolyfill(polyfill.desc.key)) {
debug(polyfill.desc.key);
utils.injectGlobalImport(polyfill.desc.module);
return true;
}
return false;
},
};
}

View File

@@ -47,7 +47,7 @@ module.exports.emptyPackages = ({ latestBuild, isHassioBuild }) =>
module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
__DEV__: !isProdBuild,
__BUILD__: JSON.stringify(latestBuild ? "modern" : "legacy"),
__BUILD__: JSON.stringify(latestBuild ? "latest" : "es5"),
__VERSION__: JSON.stringify(env.version()),
__DEMO__: false,
__SUPERVISOR__: false,
@@ -79,12 +79,7 @@ module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
sourceMap: !isTestBuild,
});
module.exports.babelOptions = ({
latestBuild,
isProdBuild,
isTestBuild,
sw,
}) => ({
module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
babelrc: false,
compact: false,
assumptions: {
@@ -92,13 +87,13 @@ module.exports.babelOptions = ({
setPublicClassFields: true,
setSpreadProperties: true,
},
browserslistEnv: latestBuild ? "modern" : `legacy${sw ? "-sw" : ""}`,
browserslistEnv: latestBuild ? "modern" : "legacy",
presets: [
[
"@babel/preset-env",
{
useBuiltIns: "usage",
corejs: dependencies["core-js"],
useBuiltIns: latestBuild ? false : "usage",
corejs: latestBuild ? false : dependencies["core-js"],
bugfixes: true,
shippedProposals: true,
},
@@ -140,14 +135,8 @@ module.exports.babelOptions = ({
"@babel/plugin-transform-runtime",
{ version: dependencies["@babel/runtime"] },
],
// Transpile decorators (still in TC39 process)
// Modern browsers support class fields and private methods, but transform is required with the older decorator version dictated by Lit
[
"@babel/plugin-proposal-decorators",
{ version: "2018-09", decoratorsBeforeExport: true },
],
"@babel/plugin-transform-class-properties",
"@babel/plugin-transform-private-methods",
// Support some proposals still in TC39 process
["@babel/plugin-proposal-decorators", { decoratorsBeforeExport: true }],
].filter(Boolean),
exclude: [
// \\ for Windows, / for Mac OS and Linux
@@ -158,24 +147,14 @@ module.exports.babelOptions = ({
overrides: [
{
// Add plugin to inject various polyfills, excluding the polyfills
// themselves to prevent self-injection.
// themselves to prevent self- injection.
plugins: [
[
path.join(BABEL_PLUGINS, "custom-polyfill-plugin.js"),
{ method: "usage-global" },
],
],
exclude: [
path.join(paths.polymer_dir, "src/resources/polyfills"),
...[
"@formatjs/(?:ecma402-abstract|intl-\\w+)",
"@lit-labs/virtualizer/polyfills",
"@webcomponents/scoped-custom-element-registry",
"element-internals-polyfill",
"proxy-polyfill",
"unfetch",
].map((p) => new RegExp(`/node_modules/${p}/`)),
],
exclude: /\/node_modules\/(?:unfetch|proxy-polyfill)\//,
},
{
// Use unambiguous for dependencies so that require() is correctly injected into CommonJS files
@@ -226,13 +205,7 @@ module.exports.config = {
return {
name: "frontend" + nameSuffix(latestBuild),
entry: {
"service-worker":
!env.useRollup() && !latestBuild
? {
import: "./src/entrypoints/service-worker.ts",
layer: "sw",
}
: "./src/entrypoints/service-worker.ts",
service_worker: "./src/entrypoints/service_worker.ts",
app: "./src/entrypoints/app.ts",
authorize: "./src/entrypoints/authorize.ts",
onboarding: "./src/entrypoints/onboarding.ts",

View File

@@ -32,7 +32,4 @@ module.exports = {
}
return version[1];
},
isDevContainer() {
return process.env.DEV_CONTAINER === "1";
},
};

View File

@@ -1,68 +1,19 @@
// Tasks to compress
import { constants } from "node:zlib";
import gulp from "gulp";
import brotli from "gulp-brotli";
import zopfli from "gulp-zopfli-green";
import paths from "../paths.cjs";
const filesGlob = "*.{js,json,css,svg,xml}";
const brotliOptions = {
skipLarger: true,
params: {
[constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY,
},
};
const zopfliOptions = { threshold: 150 };
const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
const compressDist = (rootDir) =>
gulp
.src(
[
`${modernDir}/**/${filesGlob}`,
compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined,
].filter(Boolean),
{
base: rootDir,
}
)
.pipe(brotli(brotliOptions))
.pipe(gulp.dest(rootDir));
const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
gulp
.src(
[
`${rootDir}/**/${filesGlob}`,
compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
`!${rootDir}/{sw-modern,service_worker}.js`,
`${rootDir}/{authorize,onboarding}.html`,
].filter(Boolean),
{ base: rootDir }
)
.src([
`${rootDir}/**/*.{js,json,css,svg,xml}`,
`${rootDir}/{authorize,onboarding}.html`,
])
.pipe(zopfli(zopfliOptions))
.pipe(gulp.dest(rootDir));
const compressAppBrotli = () =>
compressDistBrotli(paths.app_output_root, paths.app_output_latest);
const compressHassioBrotli = () =>
compressDistBrotli(
paths.hassio_output_root,
paths.hassio_output_latest,
false
);
const compressAppZopfli = () =>
compressDistZopfli(paths.app_output_root, paths.app_output_latest);
const compressHassioZopfli = () =>
compressDistZopfli(
paths.hassio_output_root,
paths.hassio_output_latest,
true
);
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
gulp.task(
"compress-hassio",
gulp.parallel(compressHassioBrotli, compressHassioZopfli)
);
gulp.task("compress-app", () => compressDist(paths.app_output_root));
gulp.task("compress-hassio", () => compressDist(paths.hassio_output_root));

View File

@@ -1,76 +1,28 @@
// Tasks to generate entry HTML
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 { dirname, extname, resolve } from "node:path";
import path from "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(),
// Resolve any child/nested templates relative to the parent and pass the same data
renderTemplate: (childTemplate) =>
renderTemplate(resolve(dirname(templateFile), childTemplate), data),
renderTemplate(
path.resolve(path.dirname(templateFile), childTemplate),
data
),
});
};
@@ -104,12 +56,10 @@ const genPagesDevTask =
publicRoot = ""
) =>
async () => {
const commonVars = getCommonTemplateVars();
for (const [page, entries] of Object.entries(pageEntries)) {
const content = renderTemplate(
resolve(inputRoot, inputSub, `${page}.template`),
path.resolve(inputRoot, inputSub, `${page}.template`),
{
...commonVars,
latestEntryJS: entries.map((entry) =>
useWDS
? `http://localhost:8000/src/entrypoints/${entry}.ts`
@@ -124,7 +74,7 @@ const genPagesDevTask =
es5CustomPanelJS: `${publicRoot}/frontend_es5/custom-panel.js`,
}
);
fs.outputFileSync(resolve(outputRoot, page), content);
fs.outputFileSync(path.resolve(outputRoot, page), content);
}
};
@@ -141,18 +91,16 @@ const genPagesProdTask =
) =>
async () => {
const latestManifest = fs.readJsonSync(
resolve(outputLatest, "manifest.json")
path.resolve(outputLatest, "manifest.json")
);
const es5Manifest = outputES5
? fs.readJsonSync(resolve(outputES5, "manifest.json"))
? fs.readJsonSync(path.resolve(outputES5, "manifest.json"))
: {};
const commonVars = getCommonTemplateVars();
const minifiedHTML = [];
for (const [page, entries] of Object.entries(pageEntries)) {
const content = renderTemplate(
resolve(inputRoot, inputSub, `${page}.template`),
path.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"],
@@ -160,8 +108,8 @@ const genPagesProdTask =
}
);
minifiedHTML.push(
minifyHtml(content, extname(page)).then((minified) =>
fs.outputFileSync(resolve(outputRoot, page), minified)
minifyHtml(content, path.extname(page)).then((minified) =>
fs.outputFileSync(path.resolve(outputRoot, page), minified)
)
);
}

View File

@@ -60,12 +60,6 @@ function copyPolyfills(staticDir) {
npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
staticPath("polyfills/")
);
// dialog-polyfill css
copyFileDir(
npmPath("dialog-polyfill/dialog-polyfill.css"),
staticPath("polyfills/")
);
}
function copyLoaderJS(staticDir) {

View File

@@ -24,11 +24,8 @@ const convertToJSON = async (
) => {
let localeData;
try {
// use "pt" for "pt-BR", because "pt-BR" is unsupported by @formatjs
const language = lang === "pt-BR" ? "pt" : lang;
localeData = await readFile(
join(formatjsDir, pkg, subDir, `${language}.js`),
join(formatjsDir, pkg, subDir, `${lang}.js`),
"utf-8"
);
} catch (e) {

View File

@@ -1,19 +1,20 @@
// Generate service workers
// Generate service worker.
// Based on manifest, create a file with the content as service_worker.js
import { deleteAsync } from "del";
import fs from "fs-extra";
import gulp from "gulp";
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { basename, join, relative } from "node:path";
import { injectManifest } from "workbox-build";
import path from "path";
import sourceMapUrl from "source-map-url";
import workboxBuild from "workbox-build";
import paths from "../paths.cjs";
const SW_MAP = {
[paths.app_output_latest]: "modern",
[paths.app_output_es5]: "legacy",
};
const swDest = path.resolve(paths.app_output_root, "service_worker.js");
const SW_DEV =
`
const writeSW = (content) => fs.outputFileSync(swDest, content.trim() + "\n");
gulp.task("gen-service-worker-app-dev", (done) => {
writeSW(
`
console.debug('Service worker disabled in development');
self.addEventListener('install', (event) => {
@@ -21,67 +22,72 @@ self.addEventListener('install', (event) => {
// removing any prod service worker the dev might have running
self.skipWaiting();
});
`.trim() + "\n";
gulp.task("gen-service-worker-app-dev", async () => {
await mkdir(paths.app_output_root, { recursive: true });
await Promise.all(
Object.values(SW_MAP).map((build) =>
writeFile(join(paths.app_output_root, `sw-${build}.js`), SW_DEV, {
encoding: "utf-8",
})
)
`
);
done();
});
gulp.task("gen-service-worker-app-prod", () =>
Promise.all(
Object.entries(SW_MAP).map(async ([outPath, build]) => {
const manifest = JSON.parse(
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,
injectionPoint: "__WB_MANIFEST__",
// Files that mach this pattern will be considered unique and skip revision check
// ignore JS files + translation files
dontCacheBustURLsMatching: new RegExp(
`(?:${buildDir}/.+|static/translations/.+)`
),
globDirectory: paths.app_output_root,
globPatterns: [
`${buildDir}/*.js`,
// Cache all English translations because we catch them as fallback
// Using pattern to match hash instead of * to avoid caching en-GB
// 'v' added as valid hash letter because in dev we hash with 'dev'
"static/translations/**/en-+([a-fv0-9]).json",
// Icon shown on splash screen
"static/icons/favicon-192x192.png",
"static/icons/favicon.ico",
// Common fonts
"static/fonts/roboto/Roboto-Light.woff2",
"static/fonts/roboto/Roboto-Medium.woff2",
"static/fonts/roboto/Roboto-Regular.woff2",
"static/fonts/roboto/Roboto-Bold.woff2",
],
globIgnores: [`${buildDir}/service-worker*`],
});
if (warnings.length > 0) {
console.warn(
`Problems while injecting ${build} service worker:\n`,
warnings.join("\n")
);
}
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);
}
})
)
);
gulp.task("gen-service-worker-app-prod", async () => {
// Read bundled source file
const bundleManifestLatest = fs.readJsonSync(
path.resolve(paths.app_output_latest, "manifest.json")
);
let serviceWorkerContent = fs.readFileSync(
paths.app_output_root + bundleManifestLatest["service_worker.js"],
"utf-8"
);
// Delete old file from frontend_latest so manifest won't pick it up
fs.removeSync(
paths.app_output_root + bundleManifestLatest["service_worker.js"]
);
fs.removeSync(
paths.app_output_root + bundleManifestLatest["service_worker.js.map"]
);
// Remove ES5
const bundleManifestES5 = fs.readJsonSync(
path.resolve(paths.app_output_es5, "manifest.json")
);
fs.removeSync(paths.app_output_root + bundleManifestES5["service_worker.js"]);
fs.removeSync(
paths.app_output_root + bundleManifestES5["service_worker.js.map"]
);
const workboxManifest = await workboxBuild.getManifest({
// Files that mach this pattern will be considered unique and skip revision check
// ignore JS files + translation files
dontCacheBustURLsMatching: /(frontend_latest\/.+|static\/translations\/.+)/,
globDirectory: paths.app_output_root,
globPatterns: [
"frontend_latest/*.js",
// Cache all English translations because we catch them as fallback
// Using pattern to match hash instead of * to avoid caching en-GB
// 'v' added as valid hash letter because in dev we hash with 'dev'
"static/translations/**/en-+([a-fv0-9]).json",
// Icon shown on splash screen
"static/icons/favicon-192x192.png",
"static/icons/favicon.ico",
// Common fonts
"static/fonts/roboto/Roboto-Light.woff2",
"static/fonts/roboto/Roboto-Medium.woff2",
"static/fonts/roboto/Roboto-Regular.woff2",
"static/fonts/roboto/Roboto-Bold.woff2",
],
});
for (const warning of workboxManifest.warnings) {
console.warn(warning);
}
// remove source map and add WB manifest
serviceWorkerContent = sourceMapUrl.removeFrom(serviceWorkerContent);
serviceWorkerContent = serviceWorkerContent.replace(
"WB_MANIFEST",
JSON.stringify(workboxManifest.manifestEntries)
);
// Write new file to root
fs.writeFileSync(swDest, serviceWorkerContent);
});

View File

@@ -19,7 +19,6 @@ const inBackendDir = "translations/backend";
const workDir = "build/translations";
const outDir = join(workDir, "output");
const EN_SRC = join(paths.translations_src, "en.json");
const TEST_LOCALE = "en-x-test";
let mergeBackend = false;
@@ -151,7 +150,7 @@ const createTestTranslation = () =>
: gulp
.src(EN_SRC)
.pipe(new CustomJSON(null, testReviver))
.pipe(rename(`${TEST_LOCALE}.json`))
.pipe(rename("test.json"))
.pipe(gulp.dest(workDir));
/**
@@ -193,7 +192,7 @@ const createTranslations = async () => {
// each locale, then fragmentizes and flattens the data for final output.
const translationFiles = await glob([
`${inFrontendDir}/!(en).json`,
...(env.isProdBuild() ? [] : [`${workDir}/${TEST_LOCALE}.json`]),
...(env.isProdBuild() ? [] : [`${workDir}/test.json`]),
]);
const hashStream = new Transform({
objectMode: true,
@@ -244,19 +243,19 @@ const createTranslations = async () => {
// TODO: This is a naive interpretation of BCP47 that should be improved.
// Will be OK for now as long as we don't have anything more complicated
// than a base translation + region.
const masterStream = gulp
gulp
.src(`${workDir}/en.json`)
.pipe(new PassThrough({ objectMode: true }));
masterStream.pipe(hashStream, { end: false });
const mergesFinished = [finished(masterStream)];
.pipe(new PassThrough({ objectMode: true }))
.pipe(hashStream, { end: false });
const mergesFinished = [];
for (const translationFile of translationFiles) {
const locale = basename(translationFile, ".json");
const subtags = locale.split("-");
const mergeFiles = [];
for (let i = 1; i <= subtags.length; i++) {
const lang = subtags.slice(0, i).join("-");
if (lang === TEST_LOCALE) {
mergeFiles.push(`${workDir}/${TEST_LOCALE}.json`);
if (lang === "test") {
mergeFiles.push(`${workDir}/test.json`);
} else if (lang !== "en") {
mergeFiles.push(`${inFrontendDir}/${lang}.json`);
if (mergeBackend) {
@@ -285,7 +284,7 @@ const writeTranslationMetaData = () =>
new CustomJSON((meta) => {
// Add the test translation in development.
if (!env.isProdBuild()) {
meta[TEST_LOCALE] = { nativeName: "Translation Test" };
meta.test = { nativeName: "Test" };
}
// Filter out locales without a native name, and add the hashes.
for (const locale of Object.keys(meta)) {

View File

@@ -40,12 +40,8 @@ const runDevServer = async ({
compiler,
contentBase,
port,
listenHost = undefined,
listenHost = "localhost",
}) => {
if (listenHost === undefined) {
// For dev container, we need to listen on all hosts
listenHost = env.isDevContainer() ? "0.0.0.0" : "localhost";
}
const server = new WebpackDevServer(
{
hot: false,

View File

@@ -63,25 +63,17 @@ const createWebpackConfig = ({
rules: [
{
test: /\.m?js$|\.ts$/,
use: (info) => ({
use: {
loader: "babel-loader",
options: {
...bundle.babelOptions({
latestBuild,
isProdBuild,
isTestBuild,
sw: info.issuerLayer === "sw",
}),
...bundle.babelOptions({ latestBuild, isProdBuild, isTestBuild }),
cacheDirectory: !isProdBuild,
cacheCompression: false,
},
}),
},
resolve: {
fullySpecified: false,
},
parser: {
worker: ["*context.audioWorklet.addModule()", "..."],
},
},
{
test: /\.css$/,
@@ -100,15 +92,11 @@ const createWebpackConfig = ({
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
splitChunks: {
// Disable splitting for web workers and worklets because imports of
// external chunks are broken for:
// - ESM output: https://github.com/webpack/webpack/issues/17014
// - Worklets use `importScripts`: https://github.com/webpack/webpack/issues/11543
chunks: (chunk) =>
!chunk.canBeInitial() &&
!new RegExp(`^.+-work${latestBuild ? "(?:let|er)" : "let"}$`).test(
chunk.name
),
// Disable splitting for web workers with ESM output
// Imports of external chunks are broken
chunks: latestBuild
? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name)
: undefined,
},
},
plugins: [
@@ -240,7 +228,6 @@ const createWebpackConfig = ({
),
},
experiments: {
layers: true,
outputModule: true,
},
};

View File

@@ -1,5 +0,0 @@
"use strict";
self.addEventListener("fetch", (event) => {
event.respondWith(fetch(event.request));
});

View File

@@ -36,7 +36,13 @@
</head>
<body>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
<script>
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<hc-layout subtitle="FAQ">
<style>
a {
@@ -139,7 +145,7 @@
</p>
</div>
<div class="section-header">What does Home Assistant Cast do?</div>
<div class="section-header">Wat does Home Assistant Cast do?</div>
<div class="card-content">
<p>
Home Assistant Cast is a receiver application for the Chromecast. When
@@ -226,5 +232,17 @@ http:
</p>
</div>
</hc-layout>
<script>
var _gaq = [["_setAccount", "UA-57927901-9"], ["_trackPageview"]];
(function (d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
("https:" == location.protocol ? "//ssl" : "//www") +
".google-analytics.com/ga.js";
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>
</body>
</html>

View File

@@ -13,9 +13,15 @@
<%= renderTemplate("_social_meta.html.template") %>
</head>
<body>
<hc-connect></hc-connect>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
<hc-connect></hc-connect>
<script>
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),

View File

@@ -14,10 +14,22 @@
--background-color: #41bdf5;
}
</style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</head>
<body>
<cast-media-player></cast-media-player>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
<cast-media-player></cast-media-player>
<script>
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
</body>
</html>

View File

@@ -11,4 +11,10 @@
font-size: initial;
}
</style>
<script>
var _gaq=[['_setAccount','UA-57927901-10'],['_trackPageview']];
(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];
g.src=('https:'==location.protocol?'//ssl':'//www')+'.google-analytics.com/ga.js';
s.parentNode.insertBefore(g,s)}(document,'script'));
</script>
</html>

View File

@@ -1,3 +1,4 @@
import "../../../src/resources/safari-14-attachshadow-patch";
import "./layout/hc-connect";
import("../../../src/resources/ha-style");

View File

@@ -1,7 +1,7 @@
import "@material/mwc-button/mwc-button";
import "@material/mwc-list/mwc-list";
import type { ActionDetail } from "@material/mwc-list/mwc-list";
import { mdiCast, mdiCastConnected, mdiViewDashboard } from "@mdi/js";
import { mdiCast, mdiCastConnected } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-listbox/paper-listbox";
import { Auth, Connection } from "home-assistant-js-websocket";
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -28,7 +28,6 @@ import { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
import "../../../../src/layouts/hass-loading-screen";
import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
import "./hc-layout";
import "../../../../src/components/ha-list-item";
@customElement("hc-cast")
class HcCast extends LitElement {
@@ -84,38 +83,34 @@ class HcCast extends LitElement {
`
: html`
<div class="section-header">PICK A VIEW</div>
<mwc-list @action=${this._handlePickView} activatable>
<paper-listbox
attr-for-selected="data-path"
.selected=${this.castManager.status.lovelacePath || ""}
>
${(
this.lovelaceViews ?? [
generateDefaultViewConfig({}, {}, {}, {}, () => ""),
]
).map(
(view, idx) => html`
<ha-list-item
graphic="avatar"
.activated=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
.selected=${this.castManager.status?.lovelacePath ===
(view.path ?? idx)}
<paper-icon-item
@click=${this._handlePickView}
data-path=${view.path || idx}
>
${view.title || view.path || "Unnamed view"}
${view.icon
? html`
<ha-icon
.icon=${view.icon}
slot="graphic"
slot="item-icon"
></ha-icon>
`
: html`<ha-svg-icon
slot="item-icon"
.path=${mdiViewDashboard}
></ha-svg-icon>`}
</ha-list-item>
: ""}
${view.title || view.path}
</paper-icon-item>
`
)}</mwc-list
>
)}
</paper-listbox>
`}
<div class="card-actions">
${this.castManager.status
? html`
@@ -187,8 +182,8 @@ class HcCast extends LitElement {
this.castManager.requestSession();
}
private async _handlePickView(ev: CustomEvent<ActionDetail>) {
const path = this.lovelaceViews![ev.detail.index].path ?? ev.detail.index;
private async _handlePickView(ev: Event) {
const path = (ev.currentTarget as any).getAttribute("data-path");
await ensureConnectedCastSession(this.castManager!, this.auth!);
castSendShowLovelaceView(this.castManager, this.auth.data.hassUrl, path);
}
@@ -251,14 +246,25 @@ class HcCast extends LitElement {
height: 18px;
}
ha-list-item ha-icon,
ha-list-item ha-svg-icon {
paper-listbox {
padding-top: 0;
}
paper-listbox ha-icon {
padding: 12px;
color: var(--secondary-text-color);
}
:host([hide-icons]) ha-icon {
display: none;
paper-icon-item {
cursor: pointer;
}
paper-icon-item[disabled] {
cursor: initial;
}
:host([hide-icons]) paper-icon-item {
--paper-item-icon-width: 0px;
}
.spacer {

View File

@@ -88,7 +88,7 @@ class HcLayout extends LitElement {
}
.card-header {
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -1,11 +1,9 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { LovelaceConfig } from "../../../../src/data/lovelace/config/types";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { Lovelace } from "../../../../src/panels/lovelace/types";
import "../../../../src/panels/lovelace/views/hui-view";
import "../../../../src/panels/lovelace/views/hui-view-container";
import { HomeAssistant } from "../../../../src/types";
import "./hc-launch-screen";
@@ -23,6 +21,8 @@ class HcLovelace extends LitElement {
@property() public urlPath: string | null = null;
@query("hui-view") private _huiView?: HTMLElement;
protected render(): TemplateResult {
const index = this._viewIndex;
if (index === undefined) {
@@ -44,24 +44,13 @@ class HcLovelace extends LitElement {
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
const viewConfig = this.lovelaceConfig.views[index];
const background = viewConfig.background || this.lovelaceConfig.background;
return html`
<hui-view-container
<hui-view
.hass=${this.hass}
.background=${background}
.theme=${viewConfig.theme}
>
<hui-view
.hass=${this.hass}
.lovelace=${lovelace}
.index=${index}
></hui-view>
</hui-view-container>
.lovelace=${lovelace}
.index=${index}
></hui-view>
`;
}
@@ -72,12 +61,7 @@ class HcLovelace extends LitElement {
const index = this._viewIndex;
if (index !== undefined) {
const title = getPanelTitleFromUrlPath(
this.hass,
this.urlPath || "lovelace"
);
const dashboardTitle = title || this.urlPath;
const dashboardTitle = this.lovelaceConfig.title || this.urlPath;
const viewTitle =
this.lovelaceConfig.views[index].title ||
@@ -91,6 +75,19 @@ class HcLovelace extends LitElement {
}${viewTitle || ""}`
: undefined,
});
const configBackground =
this.lovelaceConfig.views[index].background ||
this.lovelaceConfig.background;
if (configBackground) {
this._huiView!.style.setProperty(
"--lovelace-background",
configBackground
);
} else {
this._huiView!.style.removeProperty("--lovelace-background");
}
}
}
}
@@ -114,15 +111,19 @@ class HcLovelace extends LitElement {
static get styles(): CSSResultGroup {
return css`
hui-view-container {
display: flex;
position: relative;
:host {
min-height: 100vh;
height: 0;
display: flex;
flex-direction: column;
box-sizing: border-box;
background: var(--primary-background-color);
}
:host > * {
flex: 1;
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
background: var(--lovelace-background, var(--primary-background-color));
}
`;
}

View File

@@ -35,8 +35,6 @@ import { loadLovelaceResources } from "../../../../src/panels/lovelace/common/lo
import { HassElement } from "../../../../src/state/hass-element";
import { castContext } from "../cast_context";
import "./hc-launch-screen";
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
strategy: {
@@ -361,14 +359,8 @@ export class HcMain extends HassElement {
}
private _handleNewLovelaceConfig(lovelaceConfig: LovelaceConfig) {
const title = getPanelTitleFromUrlPath(
this.hass!,
this._urlPath || "lovelace"
);
castContext.setApplicationState(title || "");
this._lovelaceConfig = checkLovelaceConfig(
lovelaceConfig
) as LovelaceConfig;
castContext.setApplicationState(lovelaceConfig.title || "");
this._lovelaceConfig = lovelaceConfig;
}
private _handleShowDemo(_msg: ShowDemoMessage) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -1,5 +0,0 @@
"use strict";
self.addEventListener("fetch", (event) => {
event.respondWith(fetch(event.request));
});

View File

@@ -1,7 +1,7 @@
import { convertEntities } from "../../../../src/fake_data/entity";
import { DemoConfig } from "../types";
export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
export const demoEntitiesSections: DemoConfig["entities"] = () =>
convertEntities({
"cover.living_room_garden_shutter": {
entity_id: "cover.living_room_garden_shutter",
@@ -111,70 +111,13 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
friendly_name: "Living room Temperature",
},
},
"sensor.living_room_humidity": {
entity_id: "sensor.living_room_humidity",
state: "57",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Living room Humidity",
},
},
"sensor.outdoor_temperature": {
entity_id: "sensor.outdoor_temperature",
state: "10.5",
attributes: {
state_class: "measurement",
unit_of_measurement: "°C",
device_class: "temperature",
friendly_name: "Outdoor temperature",
},
},
"sensor.outdoor_humidity": {
entity_id: "sensor.outdoor_humidity",
state: "70.4",
attributes: {
state_class: "measurement",
unit_of_measurement: "%",
device_class: "humidity",
friendly_name: "Outdoor humidity",
},
},
"device_tracker.car": {
entity_id: "sensor.outdoor_humidity",
state: "not_home",
attributes: {
friendly_name: "Car",
icon: "mdi:car",
},
},
"media_player.living_room_nest_mini": {
entity_id: "media_player.living_room_nest_mini",
state: "playing",
state: "off",
attributes: {
device_class: "speaker",
volume_level: 0.18,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.living_room_nest_mini"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
friendly_name: "Living room Nest Mini",
supported_features: 152461,
},
},
"cover.kitchen_shutter": {
@@ -199,14 +142,6 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
supported_features: 32,
},
},
"binary_sensor.kitchen_motion": {
entity_id: "light.kitchen_motion",
state: "on",
attributes: {
device_class: "motion",
friendly_name: "Kitchen motion",
},
},
"light.worktop_spotlights": {
entity_id: "light.worktop_spotlights",
state: "off",
@@ -233,27 +168,8 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
state: "on",
attributes: {
device_class: "speaker",
volume_level: 0.18,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.kitchen_nest_audio"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
friendly_name: "Kitchen Nest Audio",
supported_features: 152461,
},
},
"binary_sensor.tesla_wall_connector_vehicle_connected": {
@@ -417,36 +333,8 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
entity_id: "media_player.study_nest_hub",
state: "off",
attributes: {
device_class: "speaker",
volume_level: 0.18,
is_volume_muted: false,
media_content_type: "music",
media_duration: 300,
media_position: 0,
media_position_updated_at: new Date(
// 23 seconds in
new Date().getTime() - 23000
).toISOString(),
media_title: "I Wasn't Born To Follow",
media_artist: "The Byrds",
media_album_name: "The Notorious Byrd Brothers",
source_list: ["It's A Party", "Radio HSL", "Retro 70s and 80s"],
shuffle: false,
night_sound: false,
speech_enhance: false,
friendly_name: localize(
"ui.panel.page-demo.config.sections.entities.media_player.study_nest_hub"
),
entity_picture: "/assets/sections/images/media_player_family_room.jpg",
supported_features: 64063,
},
},
"switch.in_meeting": {
entity_id: "switch.in_meeting",
state: "on",
attributes: {
icon: "mdi:laptop-account",
friendly_name: "In a meeting",
friendly_name: "Study Nest Hub",
supported_features: 152461,
},
},
"sensor.standing_desk_height": {

View File

@@ -1,64 +1,39 @@
import { isFrontpageEmbed } from "../../util/is_frontpage";
import { DemoConfig } from "../types";
export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
export const demoLovelaceSections: DemoConfig["lovelace"] = () => ({
title: "Home Assistant Demo",
views: [
{
type: "sections",
title: isFrontpageEmbed ? "Home Assistant" : "Demo",
title: "Demo",
path: "home",
icon: "mdi:home-assistant",
badges: [
{
type: "entity",
entity: "sensor.outdoor_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.outdoor_humidity",
color: "indigo",
},
{
type: "entity",
entity: "device_tracker.car",
},
],
sections: [
...(isFrontpageEmbed
? []
: [
{
cards: [
{
type: "heading",
heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
},
{ type: "custom:ha-demo-card" },
],
},
]),
{
title: "Welcome 👋",
cards: [{ type: "custom:ha-demo-card" }],
},
{
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.living_room"
),
icon: "mdi:sofa",
badges: [
{
type: "entity",
entity: "sensor.living_room_temperature",
color: "red",
},
{
type: "entity",
entity: "sensor.living_room_humidity",
color: "indigo",
},
],
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Garden",
},
{
type: "tile",
entity: "cover.living_room_graveyard_shutter",
name: "Rear",
},
{
type: "tile",
entity: "cover.living_room_left_shutter",
name: "Left",
},
{
type: "tile",
entity: "cover.living_room_right_shutter",
name: "Right",
},
{
type: "tile",
@@ -79,34 +54,23 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
entity: "light.bar_lamp",
},
{
type: "tile",
entity: "cover.living_room_garden_shutter",
name: "Blinds",
graph: "line",
type: "sensor",
entity: "sensor.living_room_temperature",
detail: 1,
name: "Temperature",
},
{
type: "tile",
entity: "media_player.living_room_nest_mini",
name: "Nest Mini",
},
],
title: "🛋️ Living room ",
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.kitchen"
),
icon: "mdi:fridge",
badges: [
{
type: "entity",
entity: "binary_sensor.kitchen_motion",
show_state: false,
color: "blue",
},
],
},
{
type: "tile",
entity: "cover.kitchen_shutter",
@@ -135,19 +99,14 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
{
type: "tile",
entity: "media_player.kitchen_nest_audio",
name: "Nest Audio",
},
],
title: "👩‍🍳 Kitchen",
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.energy"
),
icon: "mdi:transmission-tower",
},
{
type: "tile",
entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
@@ -185,17 +144,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "dark-grey",
},
],
title: "⚡️ Energy",
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.climate"
),
icon: "mdi:thermometer",
},
{
type: "tile",
entity: "sun.sun",
@@ -228,38 +181,16 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
state_content: ["preset_mode", "current_temperature"],
},
],
title: "🌤️ Climate",
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.study"
),
icon: "mdi:desk-lamp",
badges: [
{
type: "entity",
entity: "switch.in_meeting",
state: "on",
state_content: "name",
visibility: [
{
condition: "state",
state: "on",
entity: "switch.in_meeting",
},
],
},
],
},
{
type: "tile",
entity: "cover.study_shutter",
name: "Shutter",
},
{
type: "tile",
entity: "light.study_spotlights",
@@ -268,6 +199,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
{
type: "tile",
entity: "media_player.study_nest_hub",
name: "Nest Hub",
},
{
type: "tile",
@@ -276,23 +208,12 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
color: "brown",
icon: "mdi:desk",
},
{
type: "tile",
entity: "switch.in_meeting",
name: "Meeting mode",
},
],
title: "🧑‍💻 Study",
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.outdoor"
),
icon: "mdi:tree",
},
{
type: "tile",
entity: "light.outdoor_light",
@@ -322,17 +243,11 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
name: "Illuminance",
},
],
title: "🌳 Outdoor",
},
{
type: "grid",
cards: [
{
type: "heading",
heading: localize(
"ui.panel.page-demo.config.sections.titles.updates"
),
icon: "mdi:update",
},
{
type: "tile",
entity: "automation.home_assistant_auto_update",
@@ -358,6 +273,7 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
icon: "mdi:home-assistant",
},
],
title: "🎉 Updates",
},
],
},

View File

@@ -1,9 +1,8 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-card";
import "../../../src/components/ha-button";
import "../../../src/components/ha-circular-progress";
import { LovelaceCardConfig } from "../../../src/data/lovelace/config/card";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
@@ -12,6 +11,7 @@ import {
demoConfigs,
selectedDemoConfig,
selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs";
@customElement("ha-demo-card")
@@ -64,9 +64,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
)}
</div>
<ha-button @click=${this._nextConfig} .disabled=${this._switching}>
<mwc-button @click=${this._nextConfig} .disabled=${this._switching}>
${this.hass.localize("ui.panel.page-demo.cards.demo.next_demo")}
</ha-button>
</mwc-button>
</div>
<div class="content">
<p class="small-hidden">
@@ -87,9 +87,9 @@ export class HADemoCard extends LitElement implements LovelaceCard {
</div>
<div class="actions small-hidden">
<a href="https://www.home-assistant.io" target="_blank">
<ha-button>
<mwc-button>
${this.hass.localize("ui.panel.page-demo.cards.demo.learn_more")}
</ha-button>
</mwc-button>
</a>
</div>
</ha-card>
@@ -113,7 +113,13 @@ export class HADemoCard extends LitElement implements LovelaceCard {
private async _updateConfig(index: number) {
this._switching = true;
fireEvent(this, "set-demo-config" as any, { index });
try {
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err: any) {
alert("Failed to switch config :-(");
} finally {
this._switching = false;
}
}
static get styles(): CSSResultGroup {
@@ -143,7 +149,7 @@ export class HADemoCard extends LitElement implements LovelaceCard {
height: 60px;
}
.picker ha-button {
.picker mwc-button {
margin-right: 8px;
}

View File

@@ -1,4 +1,4 @@
import "./util/is_frontpage";
import "../../src/resources/safari-14-attachshadow-patch";
import "./ha-demo";
import("../../src/resources/ha-style");

View File

@@ -82,8 +82,6 @@ export class HaDemo extends HomeAssistantAppEl {
has_entity_name: false,
unique_id: "co2_intensity",
options: null,
created_at: 0,
modified_at: 0,
},
{
config_entry_id: "co2signal",
@@ -102,8 +100,6 @@ export class HaDemo extends HomeAssistantAppEl {
has_entity_name: false,
unique_id: "grid_fossil_fuel_percentage",
options: null,
created_at: 0,
modified_at: 0,
},
]);

View File

@@ -63,47 +63,46 @@
align-items: center;
}
#ha-launch-screen svg {
width: 112px;
width: 170px;
flex-shrink: 0;
}
#ha-launch-screen .ha-launch-screen-spacer-top {
#ha-launch-screen .ha-launch-screen-spacer {
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 {
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-top"></div>
<div class="ha-launch-screen-spacer"></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-bottom"></div>
<div class="ohf-logo">
<img src="/static/images/ohf-badge.svg" alt="Home Assistant is a project by the Open Home Foundation" height="46">
</div>
<div id="ha-launch-screen-info-box" class="ha-launch-screen-spacer"></div>
</div>
<ha-demo></ha-demo>
<%= renderTemplate("../../../src/html/_js_base.html.template") %>
<%= renderTemplate("../../../src/html/_preload_roboto.html.template") %>
<%= renderTemplate("../../../src/html/_script_loader.html.template") %>
<script>
// Safari 12 and below does not have a compliant ES2015 implementation of template literals, so we ship ES5
if (!isS11_12) {
<% for (const entry of latestEntryJS) { %>
import("<%= entry %>");
<% } %>
window.latestJS = true;
}
</script>
<%= renderTemplate("../../../src/html/_script_load_es5.html.template") %>
<script>
var _gaq = [["_setAccount", "UA-57927901-5"], ["_trackPageview"]];
(function (d, t) {
var g = d.createElement(t),
s = d.getElementsByTagName(t)[0];
g.src =
("https:" == location.protocol ? "//ssl" : "//www") +
".google-analytics.com/ga.js";
s.parentNode.insertBefore(g, s);
})(document, "script");
</script>
</body>
</html>

View File

@@ -1,9 +0,0 @@
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockConfig = (hass: MockHomeAssistant) => {
hass.mockWS("validate_config", () => ({
actions: { valid: true },
conditions: { valid: true },
triggers: { valid: true },
}));
};

View File

@@ -1,55 +1,5 @@
import { convertEntities } from "../../../src/fake_data/entity";
export const mapEntities = () =>
convertEntities({
"zone.home": {
entity_id: "zone.home",
state: "zoning",
attributes: {
hidden: true,
latitude: 52.3631339,
longitude: 4.8903147,
radius: 200,
friendly_name: "Home",
icon: "hademo:home",
},
},
"zone.uva": {
entity_id: "zone.buckhead",
state: "zoning",
attributes: {
hidden: true,
radius: 400,
friendly_name: "UvA",
icon: "hademo:school",
latitude: 52.3558182,
longitude: 4.9535376,
},
},
"person.arsaboo": {
entity_id: "person.arsaboo",
state: "not_home",
attributes: {
radius: 50,
friendly_name: "Arsaboo",
latitude: 52.3579946,
longitude: 4.8664597,
entity_picture: "/assets/arsaboo/images/arsaboo.jpg",
},
},
"person.melody": {
entity_id: "person.melody",
state: "not_home",
attributes: {
radius: 50,
friendly_name: "Melody",
latitude: 52.3408927,
longitude: 4.8711073,
entity_picture: "/assets/arsaboo/images/melody.jpg",
},
},
});
export const energyEntities = () =>
convertEntities({
"sensor.grid_fossil_fuel_percentage": {

View File

@@ -1,52 +1,35 @@
import type { LocalizeFunc } from "../../../src/common/translations/localize";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
import {
selectedDemoConfig,
selectedDemoConfigIndex,
setDemoConfig,
} from "../configs/demo-configs";
import { selectedDemoConfig } from "../configs/demo-configs";
import "../custom-cards/cast-demo-row";
import "../custom-cards/ha-demo-card";
import { mapEntities } from "./entities";
import type { HADemoCard } from "../custom-cards/ha-demo-card";
export const mockLovelace = (
hass: MockHomeAssistant,
localizePromise: Promise<LocalizeFunc>
) => {
hass.mockWS("lovelace/config", ({ url_path }) => {
if (url_path === "map") {
hass.addEntities(mapEntities());
return {
strategy: {
type: "map",
},
};
}
return Promise.all([selectedDemoConfig, localizePromise]).then(
hass.mockWS("lovelace/config", () =>
Promise.all([selectedDemoConfig, localizePromise]).then(
([config, localize]) => config.lovelace(localize)
);
});
)
);
hass.mockWS("lovelace/config/save", () => Promise.resolve());
hass.mockWS("lovelace/resources", () => Promise.resolve([]));
};
customElements.whenDefined("hui-root").then(() => {
customElements.whenDefined("hui-view").then(() => {
// eslint-disable-next-line
const HUIRoot = customElements.get("hui-root")!;
const HUIView = customElements.get("hui-view");
// Patch HUI-VIEW to make the lovelace object available to the demo card
const oldCreateCard = HUIView!.prototype.createCardElement;
const oldFirstUpdated = HUIRoot.prototype.firstUpdated;
HUIRoot.prototype.firstUpdated = function (changedProperties) {
oldFirstUpdated.call(this, changedProperties);
this.addEventListener("set-demo-config", async (ev) => {
const index = (ev as CustomEvent).detail.index;
try {
await setDemoConfig(this.hass, this.lovelace!, index);
} catch (err: any) {
setDemoConfig(this.hass, this.lovelace!, selectedDemoConfigIndex);
alert("Failed to switch config :-(");
}
});
HUIView!.prototype.createCardElement = function (config) {
const el = oldCreateCard.call(this, config);
if (el.tagName === "HA-DEMO-CARD") {
(el as HADemoCard).lovelace = this.lovelace;
}
return el;
};
});

View File

@@ -1,6 +0,0 @@
import { Tag } from "../../../src/data/tag";
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockTags = (hass: MockHomeAssistant) => {
hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
};

View File

@@ -1 +0,0 @@
export const isFrontpageEmbed = document.location.search === "?frontpage";

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

View File

@@ -1,9 +1,7 @@
import { load } from "js-yaml";
import { LitElement, PropertyValueMap, css, html, nothing } from "lit";
import { html, css, LitElement, PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import "../../../src/panels/lovelace/cards/hui-card";
import type { HuiCard } from "../../../src/panels/lovelace/cards/hui-card";
import { createCardElement } from "../../../src/panels/lovelace/create-element/create-card-element";
import { HomeAssistant } from "../../../src/types";
export interface DemoCardConfig {
@@ -21,12 +19,7 @@ class DemoCard extends LitElement {
@state() private _size?: number;
@query("hui-card", false) private _card?: HuiCard;
private _config = memoizeOne((config: string) => {
const c = (load(config) as any)[0];
return c;
});
@query("#card") private _card!: HTMLElement;
render() {
return html`
@@ -37,32 +30,63 @@ class DemoCard extends LitElement {
: ""}
</h2>
<div class="root">
<hui-card
.config=${this._config(this.config.config)}
.hass=${this.hass}
@card-updated=${this._cardUpdated}
></hui-card>
${this.showConfig
? html`<pre>${this.config.config.trim()}</pre>`
: nothing}
<div id="card"></div>
${this.showConfig ? html`<pre>${this.config.config.trim()}</pre>` : ""}
</div>
`;
}
private async _cardUpdated(ev) {
ev.stopPropagation();
this._updateSize();
updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("config")) {
const card = this._card;
while (card.lastChild) {
card.removeChild(card.lastChild);
}
const el = this._createCardElement((load(this.config.config) as any)[0]);
card.appendChild(el);
this._getSize(el);
}
if (changedProps.has("hass")) {
const card = this._card.lastChild;
if (card) {
(card as any).hass = this.hass;
}
}
}
private async _updateSize() {
this._size = await this._card?.getCardSize();
async _getSize(el) {
await customElements.whenDefined(el.localName);
if (!("getCardSize" in el)) {
this._size = undefined;
return;
}
this._size = await el.getCardSize();
}
protected update(
_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>
): void {
super.update(_changedProperties);
this._updateSize();
_createCardElement(cardConfig) {
const element = createCardElement(cardConfig);
if (this.hass) {
element.hass = this.hass;
}
element.addEventListener(
"ll-rebuild",
(ev) => {
ev.stopPropagation();
this._rebuildCard(element, cardConfig);
},
{ once: true }
);
return element;
}
_rebuildCard(cardElToReplace, config) {
const newCardEl = this._createCardElement(config);
cardElToReplace.parentElement.replaceChild(newCardEl, cardElToReplace);
}
static styles = css`
@@ -77,7 +101,7 @@ class DemoCard extends LitElement {
font-size: 0.5em;
color: var(--primary-text-color);
}
hui-card {
#card {
max-width: 400px;
width: 100vw;
}

View File

@@ -532,6 +532,15 @@ 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

@@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = {
id: "1615419646544",
alias: "Ensure Party mode",
description: "",
triggers: [
trigger: [
{
trigger: "state",
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
conditions: [
condition: [
{
condition: "template",
alias: "Test if Paulus is home",
value_template: "{{ true }}",
},
],
actions: [
action: [
{
action: "input_boolean.toggle",
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
@@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
],
default: [
{
action: "input_boolean.toggle",
service: "input_boolean.toggle",
alias: "Toggle 2",
target: {
entity_id: "input_boolean.toggle_2",
@@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
],
},
{
action: "input_boolean.toggle",
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},

View File

@@ -31,8 +31,8 @@ export const mockDemoTrace = (
],
},
config: {
triggers: [],
actions: [],
trigger: [],
action: [],
},
context: {
id: "abcd",

View File

@@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = {
config: {
mode: "restart",
max_exceeded: "silent",
triggers: [
trigger: [
{
trigger: "state",
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from: "off",
to: "on",
},
],
actions: [
action: [
{
action: "light.turn_on",
service: "light.turn_on",
target: {
entity_id: "light.elgato_key_light_air",
},
@@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
delay: 0,
},
{
action: "light.turn_off",
service: "light.turn_off",
target: {
entity_id: "light.elgato_key_light_air",
},

View File

@@ -3,16 +3,13 @@ title: When to use remove, delete, add and create
subtitle: The difference between remove/delete and add/create.
---
# Removing or deleting content
# Remove vs Delete
_Remove_ and _Delete_ are quite similar, but can be frustrating if used inconsistently.
- Remove refers to an action that can be restored or reapplied.
- Delete refers to a permanent, non-recoverable action.
Remove and Delete are quite similar, but can be frustrating if used inconsistently.
## Remove
The term _Remove_ should always be used when an item/setting or content is to be removed or disassociated, but the action can be reversed or reapplied.
Take away and set aside, but kept in existence.
For example:
@@ -25,7 +22,7 @@ For example:
## Delete
The term _Delete_ should always be used to refer to any action that will cause the permanent deletion of an item/setting or content.
Erase, rendered nonexistent or nonrecoverable.
For example:

View File

@@ -48,7 +48,7 @@ const ACTIONS = [
{
wait_for_trigger: [
{
trigger: "state",
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
@@ -64,12 +64,6 @@ const ACTIONS = [
entity_id: "input_boolean.toggle_4",
},
},
{
sequence: [
{ scene: "scene.kitchen_morning" },
{ service: "light.turn_off", target: { entity_id: "light.kitchen" } },
],
},
{
parallel: [
{ scene: "scene.kitchen_morning" },
@@ -121,7 +115,7 @@ const ACTIONS = [
];
const initialAction: Action = {
action: "light.turn_on",
service: "light.turn_on",
target: {
entity_id: "light.kitchen",
},
@@ -142,7 +136,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action">
<span>
${this._action
? describeAction(this.hass, [], [], {}, this._action)
? describeAction(this.hass, [], [], [], this._action)
: "<invalid YAML>"}
</span>
<ha-yaml-editor
@@ -155,7 +149,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span>
<span>${describeAction(this.hass, [], [], [], conf as any)}</span>
<pre>${dump(conf)}</pre>
</div>
`

View File

@@ -22,52 +22,46 @@ const ENTITIES = [
];
const triggers = [
{ trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ trigger: "mqtt" },
{ platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
{ platform: "mqtt" },
{
trigger: "geo_location",
platform: "geo_location",
source: "test_source",
zone: "zone.home",
event: "enter",
},
{ trigger: "homeassistant", event: "start" },
{ platform: "homeassistant", event: "start" },
{
trigger: "numeric_state",
platform: "numeric_state",
entity_id: "light.kitchen",
attribute: "brightness",
below: 80,
above: 20,
},
{ trigger: "sun", event: "sunset" },
{ trigger: "time_pattern" },
{ trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
{ trigger: "webhook" },
{ trigger: "persistent_notification" },
{ platform: "sun", event: "sunset" },
{ platform: "time_pattern" },
{ platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
{ platform: "webhook" },
{ platform: "persistent_notification" },
{
trigger: "zone",
platform: "zone",
entity_id: "person.person",
zone: "zone.home",
event: "enter",
},
{ trigger: "tag" },
{ trigger: "time", at: "15:32" },
{ trigger: "template" },
{ trigger: "conversation", command: "Turn on the lights" },
{ platform: "tag" },
{ platform: "time", at: "15:32" },
{ platform: "template" },
{ platform: "conversation", command: "Turn on the lights" },
{
trigger: "conversation",
platform: "conversation",
command: ["Turn on the lights", "Turn the lights on"],
},
{ trigger: "event", event_type: "homeassistant_started" },
{
triggers: [
{ trigger: "state", entity_id: "light.kitchen", to: "on" },
{ trigger: "state", entity_id: "light.kitchen", to: "off" },
],
},
{ platform: "event", event_type: "homeassistant_started" },
];
const initialTrigger: Trigger = {
trigger: "state",
platform: "state",
entity_id: "light.kitchen",
};

View File

@@ -20,7 +20,6 @@ import { HaWaitForTriggerAction } from "../../../../src/panels/config/automation
import { HaWaitAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
import { Action } from "../../../../src/data/script";
import { HaConditionAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-condition";
import { HaSequenceAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-sequence";
import { HaParallelAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-parallel";
import { HaIfAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-if";
import { HaStopAction } from "../../../../src/panels/config/automation/action/types/ha-automation-action-stop";
@@ -40,7 +39,6 @@ const SCHEMAS: { name: string; actions: Action[] }[] = [
{ name: "If-Then", actions: [HaIfAction.defaultConfig] },
{ name: "Choose", actions: [HaChooseAction.defaultConfig] },
{ name: "Variables", actions: [{ variables: { hello: "1" } }] },
{ name: "Sequence", actions: [HaSequenceAction.defaultConfig] },
{ name: "Parallel", actions: [HaParallelAction.defaultConfig] },
{ name: "Stop", actions: [HaStopAction.defaultConfig] },
];

View File

@@ -11,6 +11,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
import type { ConditionWithShorthand } from "../../../../src/data/automation";
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
@@ -18,67 +19,62 @@ import { HaTemplateCondition } from "../../../../src/panels/config/automation/co
import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
import { HaAndCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-and";
import { HaOrCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-or";
import { HaNotCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-not";
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
{
name: "State",
conditions: [{ ...HaStateCondition.defaultConfig }],
conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
},
{
name: "Numeric State",
conditions: [{ ...HaNumericStateCondition.defaultConfig }],
conditions: [
{ condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
],
},
{
name: "Sun",
conditions: [{ ...HaSunCondition.defaultConfig }],
conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
},
{
name: "Zone",
conditions: [{ ...HaZoneCondition.defaultConfig }],
conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
},
{
name: "Time",
conditions: [{ ...HaTimeCondition.defaultConfig }],
conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
},
{
name: "Template",
conditions: [{ ...HaTemplateCondition.defaultConfig }],
conditions: [
{ condition: "template", ...HaTemplateCondition.defaultConfig },
],
},
{
name: "Device",
conditions: [{ ...HaDeviceCondition.defaultConfig }],
conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
},
{
name: "And",
conditions: [{ ...HaAndCondition.defaultConfig }],
conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Or",
conditions: [{ ...HaOrCondition.defaultConfig }],
conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Not",
conditions: [{ ...HaNotCondition.defaultConfig }],
conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
},
{
name: "Trigger",
conditions: [{ ...HaTriggerCondition.defaultConfig }],
conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
},
{
name: "Shorthand",
conditions: [
{
...HaAndCondition.defaultConfig,
},
{
...HaOrCondition.defaultConfig,
},
{
...HaNotCondition.defaultConfig,
},
{ and: HaLogicalCondition.defaultConfig.conditions },
{ or: HaLogicalCondition.defaultConfig.conditions },
{ not: HaLogicalCondition.defaultConfig.conditions },
],
},
];

View File

@@ -8,9 +8,6 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
import { mockConfig } from "../../../../demo/src/stubs/config";
import { mockTags } from "../../../../demo/src/stubs/tags";
import { mockAuth } from "../../../../demo/src/stubs/auth";
import type { Trigger } from "../../../../src/data/automation";
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
@@ -29,53 +26,59 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
name: "State",
triggers: [{ ...HaStateTrigger.defaultConfig }],
triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [{ ...HaGeolocationTrigger.defaultConfig }],
triggers: [
{ platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
],
},
{
name: "Home Assistant",
triggers: [{ ...HaHassTrigger.defaultConfig }],
triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
},
{
name: "Numeric State",
triggers: [{ ...HaNumericStateTrigger.defaultConfig }],
triggers: [
{ platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
],
},
{
name: "Sun",
triggers: [{ ...HaSunTrigger.defaultConfig }],
triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
},
{
name: "Time Pattern",
triggers: [{ ...HaTimePatternTrigger.defaultConfig }],
triggers: [
{ platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
],
},
{
name: "Webhook",
triggers: [{ ...HaWebhookTrigger.defaultConfig }],
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
},
{
name: "Persistent Notification",
triggers: [
{
platform: "persistent_notification",
...HaPersistentNotificationTrigger.defaultConfig,
},
],
@@ -83,47 +86,43 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
{
name: "Zone",
triggers: [{ ...HaZoneTrigger.defaultConfig }],
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
},
{
name: "Tag",
triggers: [{ ...HaTagTrigger.defaultConfig }],
triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
},
{
name: "Time",
triggers: [{ ...HaTimeTrigger.defaultConfig }],
triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
},
{
name: "Template",
triggers: [{ ...HaTemplateTrigger.defaultConfig }],
triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
},
{
name: "Event",
triggers: [{ ...HaEventTrigger.defaultConfig }],
triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
},
{
name: "Device Trigger",
triggers: [{ ...HaDeviceTrigger.defaultConfig }],
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
},
{
name: "Sentence",
triggers: [
{ ...HaConversationTrigger.defaultConfig },
{ platform: "conversation", ...HaConversationTrigger.defaultConfig },
{
trigger: "conversation",
platform: "conversation",
command: ["Turn on the lights", "Turn the lights on"],
},
],
},
{
name: "Trigger list",
triggers: [{ ...HaTriggerList.defaultConfig }],
},
];
@customElement("demo-automation-editor-trigger")
@@ -143,9 +142,6 @@ export class DemoAutomationEditorTrigger extends LitElement {
mockDeviceRegistry(hass);
mockAreaRegistry(hass);
mockHassioSupervisor(hass);
mockConfig(hass);
mockTags(hass);
mockAuth(hass);
}
protected render(): TemplateResult {

View File

@@ -15,7 +15,6 @@ import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../../src/types";
import "../../components/demo-black-white-row";
import { DeviceRegistryEntry } from "../../../../src/data/device_registry";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
@@ -42,7 +41,7 @@ const ENTITIES = [
}),
];
const DEVICES: DeviceRegistryEntry[] = [
const DEVICES = [
{
area_id: "bedroom",
configuration_url: null,
@@ -54,7 +53,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
@@ -62,9 +60,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null,
serial_number: null,
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: "backyard",
@@ -77,7 +72,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
@@ -85,9 +79,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null,
serial_number: null,
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: null,
@@ -100,7 +91,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,
@@ -108,9 +98,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null,
serial_number: null,
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];
@@ -123,8 +110,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
created_at: 0,
modified_at: 0,
},
{
area_id: "bedroom",
@@ -134,8 +119,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
created_at: 0,
modified_at: 0,
},
{
area_id: "livingroom",
@@ -145,8 +128,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
created_at: 0,
modified_at: 0,
},
];

View File

@@ -21,7 +21,6 @@ import { FloorRegistryEntry } from "../../../../src/data/floor_registry";
import { LabelRegistryEntry } from "../../../../src/data/label_registry";
import { mockFloorRegistry } from "../../../../demo/src/stubs/floor_registry";
import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
import { DeviceRegistryEntry } from "../../../../src/data/device_registry";
const ENTITIES = [
getEntity("alarm_control_panel", "alarm", "disarmed", {
@@ -42,7 +41,7 @@ const ENTITIES = [
}),
];
const DEVICES: DeviceRegistryEntry[] = [
const DEVICES = [
{
area_id: "bedroom",
configuration_url: null,
@@ -54,7 +53,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "volume1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: null,
name: "Dishwasher",
sw_version: null,
@@ -62,9 +60,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null,
serial_number: null,
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: "backyard",
@@ -77,7 +72,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: null,
name: "Lamp",
sw_version: null,
@@ -85,9 +79,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null,
serial_number: null,
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
{
area_id: null,
@@ -100,7 +91,6 @@ const DEVICES: DeviceRegistryEntry[] = [
identifiers: [["demo", "pwm1"] as [string, string]],
manufacturer: null,
model: null,
model_id: null,
name_by_user: "User name",
name: "Technical name",
sw_version: null,
@@ -108,9 +98,6 @@ const DEVICES: DeviceRegistryEntry[] = [
via_device_id: null,
serial_number: null,
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];
@@ -123,8 +110,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
created_at: 0,
modified_at: 0,
},
{
area_id: "bedroom",
@@ -134,8 +119,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
created_at: 0,
modified_at: 0,
},
{
area_id: "livingroom",
@@ -145,8 +128,6 @@ const AREAS: AreaRegistryEntry[] = [
picture: null,
aliases: [],
labels: [],
created_at: 0,
modified_at: 0,
},
];
@@ -157,8 +138,6 @@ const FLOORS: FloorRegistryEntry[] = [
level: 0,
icon: null,
aliases: [],
created_at: 0,
modified_at: 0,
},
{
floor_id: "first",
@@ -166,8 +145,6 @@ const FLOORS: FloorRegistryEntry[] = [
level: 1,
icon: "mdi:numeric-1",
aliases: [],
created_at: 0,
modified_at: 0,
},
{
floor_id: "second",
@@ -175,8 +152,6 @@ const FLOORS: FloorRegistryEntry[] = [
level: 2,
icon: "mdi:numeric-2",
aliases: [],
created_at: 0,
modified_at: 0,
},
];
@@ -187,8 +162,6 @@ const LABELS: LabelRegistryEntry[] = [
icon: null,
color: "yellow",
description: null,
created_at: 0,
modified_at: 0,
},
{
label_id: "entertainment",
@@ -196,8 +169,6 @@ const LABELS: LabelRegistryEntry[] = [
icon: "mdi:popcorn",
color: "blue",
description: null,
created_at: 0,
modified_at: 0,
},
];

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeNumeric extends LitElement {
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeNumeric(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeSeconds extends LitElement {
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeShortYear extends LitElement {
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTimeWithYear(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTimeShort extends LitElement {
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatShortDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeDateTime extends LitElement {
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatDateTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -35,57 +35,59 @@ export class DemoDateTimeDate extends LitElement {
<div class="center">Month-Day-Year</div>
<div class="center">Year-Month-Day</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.DMY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.MDY,
},
demoConfig
)}
</div>
<div class="center">
${formatDateNumeric(
date,
{
...defaultLocale,
language: key,
date_format: DateFormat.YMD,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeTimeSeconds extends LitElement {
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWithSeconds(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeTimeWeekday extends LitElement {
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTimeWeekday(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -56,46 +56,48 @@ export class DemoDateTimeTime extends LitElement {
<div class="center">12 Hours</div>
<div class="center">24 Hours</div>
</div>
${Object.entries(translationMetadata.translations).map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
${Object.entries(translationMetadata.translations)
.filter(([key, _]) => key !== "test")
.map(
([key, value]) => html`
<div class="container">
<div>${value.nativeName}</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.language,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.am_pm,
},
demoConfig
)}
</div>
<div class="center">
${formatTime(
this.date,
{
...defaultLocale,
language: key,
time_format: TimeFormat.twenty_four,
},
demoConfig
)}
</div>
</div>
`
)}
`
)}
</mwc-list>
`;
}

View File

@@ -287,11 +287,11 @@ const CONFIGS = [
config: `
- type: entities
entities:
- type: perform-action
- type: call-service
icon: mdi:power
name: Bed light
action_name: Toggle light
action: light.toggle
service: light.toggle
data:
entity_id: light.bed_light
- type: section

View File

@@ -1,3 +0,0 @@
---
title: Picture Card
---

View File

@@ -1,61 +0,0 @@
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, query } from "lit/decorators";
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";
const ENTITIES = [
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
entity_picture: "/images/paulus.jpg",
}),
];
const CONFIGS = [
{
heading: "Image URL",
config: `
- type: picture
image: /images/living_room.png
`,
},
{
heading: "Person entity",
config: `
- type: picture
image_entity: person.paulus
`,
},
{
heading: "Error: Image required",
config: `
- type: picture
entity: person.paulus
`,
},
];
@customElement("demo-lovelace-picture-card")
class DemoPicture extends LitElement {
@query("#demos") private _demoRoot!: HTMLElement;
protected render(): TemplateResult {
return html`<demo-cards id="demos" .configs=${CONFIGS}></demo-cards>`;
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
const hass = provideHass(this._demoRoot);
hass.updateTranslations(null, "en");
hass.updateTranslations("lovelace", "en");
hass.addEntities(ENTITIES);
mockIcons(hass);
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-lovelace-picture-card": DemoPicture;
}
}

View File

@@ -25,15 +25,6 @@ const ENTITIES = [
friendly_name: "Movement Backyard",
device_class: "motion",
}),
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
entity_picture: "/images/paulus.jpg",
}),
getEntity("sensor", "battery", 35, {
device_class: "battery",
friendly_name: "Battery",
unit_of_measurement: "%",
}),
];
const CONFIGS = [
@@ -132,19 +123,6 @@ const CONFIGS = [
left: 35%
`,
},
{
heading: "Person entity",
config: `
- type: picture-elements
image_entity: person.paulus
elements:
- type: state-icon
entity: sensor.battery
style:
top: 8%
left: 8%
`,
},
];
@customElement("demo-lovelace-picture-elements-card")

View File

@@ -12,10 +12,6 @@ const ENTITIES = [
getEntity("light", "bed_light", "off", {
friendly_name: "Bed Light",
}),
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
entity_picture: "/images/paulus.jpg",
}),
];
const CONFIGS = [
@@ -54,13 +50,6 @@ const CONFIGS = [
entity: camera.demo_camera
`,
},
{
heading: "Person entity",
config: `
- type: picture-entity
entity: person.paulus
`,
},
{
heading: "Hidden name",
config: `

View File

@@ -20,15 +20,6 @@ const ENTITIES = [
friendly_name: "Basement Floor Wet",
device_class: "moisture",
}),
getEntity("person", "paulus", "home", {
friendly_name: "Paulus",
entity_picture: "/images/paulus.jpg",
}),
getEntity("sensor", "battery", 35, {
device_class: "battery",
friendly_name: "Battery",
unit_of_measurement: "%",
}),
];
const CONFIGS = [
@@ -99,15 +90,6 @@ const CONFIGS = [
- light.ceiling_lights
`,
},
{
heading: "Person entity",
config: `
- type: picture-glance
image_entity: person.paulus
entities:
- sensor.battery
`,
},
{
heading: "Custom icon",
config: `

View File

@@ -8,7 +8,6 @@ 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", {
@@ -61,36 +60,6 @@ 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 = [
@@ -224,25 +193,6 @@ 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

@@ -140,9 +140,6 @@ const ENTITIES: HassEntity[] = [
createEntity("climate.auto_preheating", "auto", undefined, {
hvac_action: "preheating",
}),
createEntity("climate.auto_defrosting", "auto", undefined, {
hvac_action: "defrosting",
}),
createEntity("climate.auto_heating", "auto", undefined, {
hvac_action: "heating",
}),
@@ -358,18 +355,19 @@ export class DemoEntityState extends LitElement {
},
entity_id: {
title: "Entity ID",
width: "30%",
filterable: true,
sortable: true,
},
state: {
title: "State",
width: "20%",
sortable: true,
template: (entry) =>
html`${computeStateDisplay(
hass.localize,
entry.stateObj,
hass.locale,
[], // numericDeviceClasses
hass.config,
hass.entities
)}`,
@@ -377,12 +375,14 @@ export class DemoEntityState extends LitElement {
device_class: {
title: "Device class",
template: (entry) => html`${entry.device_class ?? "-"}`,
width: "20%",
filterable: true,
sortable: true,
},
domain: {
title: "Domain",
template: (entry) => html`${computeDomain(entry.entity_id)}`,
width: "20%",
filterable: true,
sortable: true,
},

View File

@@ -1,3 +0,0 @@
---
title: Markdown
---

View File

@@ -1,93 +0,0 @@
import { css, html, LitElement } from "lit";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-markdown";
import { customElement } from "lit/decorators";
interface MarkdownContent {
content: string;
breaks: boolean;
allowSvg: boolean;
lazyImages: boolean;
}
const mdContentwithDefaults = (md: Partial<MarkdownContent>) =>
({
breaks: false,
allowSvg: false,
lazyImages: false,
...md,
}) as MarkdownContent;
const generateContent = (md) => `
\`\`\`json
${JSON.stringify({ ...md, content: undefined })}
\`\`\`
---
${md.content}
`;
const markdownContents: MarkdownContent[] = [
mdContentwithDefaults({
content: "_Hello_ **there** 👋, ~~nice~~ of you ||to|| show up.",
}),
...[true, false].map((breaks) =>
mdContentwithDefaults({
breaks,
content: `
![image](https://img.shields.io/badge/markdown-rendering-brightgreen)
![image](https://img.shields.io/badge/markdown-rendering-blue)
> [!TIP]
> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer dictum quis ante eu eleifend. Integer sed [consectetur est, nec elementum magna](#). Fusce lobortis lectus ac rutrum tincidunt. Quisque suscipit gravida ante, in convallis risus vulputate non.
key | description
-- | --
lorem | ipsum
- list item 1
- list item 2
`,
})
),
];
@customElement("demo-misc-ha-markdown")
export class DemoMiscMarkdown extends LitElement {
protected render() {
return html`
<div class="container">
${markdownContents.map(
(md) =>
html`<ha-card>
<ha-markdown
.content=${generateContent(md)}
.breaks=${md.breaks}
.allowSvg=${md.allowSvg}
.lazyImages=${md.lazyImages}
></ha-markdown>
</ha-card>`
)}
</div>
`;
}
static get styles() {
return css`
ha-card {
margin: 12px;
padding: 12px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-misc-ha-markdown": DemoMiscMarkdown;
}
}

View File

@@ -203,8 +203,6 @@ const createEntityRegistryEntries = (
options: null,
labels: [],
categories: {},
created_at: 0,
modified_at: 0,
},
];
@@ -217,7 +215,6 @@ const createDeviceRegistryEntries = (
connections: [],
manufacturer: "ESPHome",
model: "Mock Device",
model_id: "ABC-001",
name: "Tag Reader",
sw_version: null,
hw_version: "1.0.0",
@@ -230,9 +227,6 @@ const createDeviceRegistryEntries = (
disabled_by: null,
configuration_url: null,
labels: [],
created_at: 0,
modified_at: 0,
primary_config_entry: null,
},
];

View File

@@ -417,7 +417,7 @@ class HassioAddonConfig extends LitElement {
justify-content: space-between;
}
.header h2 {
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;

View File

@@ -37,6 +37,7 @@ import "./config/hassio-addon-config";
import "./config/hassio-addon-network";
import "./hassio-addon-router";
import "./info/hassio-addon-info";
import "./log/hassio-addon-logs";
@customElement("hassio-addon-dashboard")
class HassioAddonDashboard extends LitElement {
@@ -160,11 +161,16 @@ class HassioAddonDashboard extends LitElement {
margin-bottom: 24px;
width: 600px;
}
hassio-addon-logs {
max-width: calc(100% - 8px);
min-width: 600px;
}
@media only screen and (max-width: 600px) {
hassio-addon-info,
hassio-addon-network,
hassio-addon-audio,
hassio-addon-config {
hassio-addon-config,
hassio-addon-logs {
max-width: 100%;
min-width: 100%;
}

View File

@@ -2,8 +2,7 @@ import "@material/mwc-button";
import {
mdiCheckCircle,
mdiChip,
mdiPlayCircle,
mdiCircleOffOutline,
mdiCircle,
mdiCursorDefaultClickOutline,
mdiDocker,
mdiExclamationThick,
@@ -38,7 +37,6 @@ import "../../../../src/components/ha-markdown";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import type { HaSwitch } from "../../../../src/components/ha-switch";
import {
AddonCapability,
HassioAddonDetails,
@@ -200,7 +198,7 @@ class HassioAddonInfo extends LitElement {
"dashboard.addon_running"
)}
class="running"
.path=${mdiPlayCircle}
.path=${mdiCircle}
></ha-svg-icon>
`
: html`
@@ -209,7 +207,7 @@ class HassioAddonInfo extends LitElement {
"dashboard.addon_stopped"
)}
class="stopped"
.path=${mdiCircleOffOutline}
.path=${mdiCircle}
></ha-svg-icon>
`}
`
@@ -1120,28 +1118,12 @@ class HassioAddonInfo extends LitElement {
private async _uninstallClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
let removeData = false;
const _removeDataToggled = (e: Event) => {
removeData = (e.target as HaSwitch).checked;
};
const confirmed = await showConfirmationDialog(this, {
title: this.supervisor.localize("dialog.uninstall_addon.title", {
name: this.addon.name,
}),
text: html`
<ha-formfield
.label=${html`<p>
${this.supervisor.localize("dialog.uninstall_addon.remove_data")}
</p>`}
>
<ha-switch
@change=${_removeDataToggled}
.checked=${removeData}
haptic
></ha-switch>
</ha-formfield>
`,
text: this.supervisor.localize("dialog.uninstall_addon.text"),
confirmText: this.supervisor.localize("dialog.uninstall_addon.uninstall"),
dismissText: this.supervisor.localize("common.cancel"),
destructive: true,
@@ -1154,7 +1136,7 @@ class HassioAddonInfo extends LitElement {
this._error = undefined;
try {
await uninstallHassioAddon(this.hass, this.addon.slug, removeData);
await uninstallHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
@@ -1209,7 +1191,7 @@ class HassioAddonInfo extends LitElement {
padding-inline-start: 8px;
padding-inline-end: initial;
font-size: 24px;
color: var(--ha-card-header-color, var(--primary-text-color));
color: var(--ha-card-header-color, --primary-text-color);
}
.addon-version {
float: var(--float-end);

View File

@@ -1,14 +1,12 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import "../../../../src/panels/config/logs/error-log-card";
import "../../../../src/components/search-input";
import { extractSearchParam } from "../../../../src/common/url/search-params";
import "./hassio-addon-logs";
@customElement("hassio-addon-log-tab")
class HassioAddonLogDashboard extends LitElement {
@@ -18,8 +16,6 @@ class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public addon?: HassioAddonDetails;
@state() private _filter = extractSearchParam("filter") || "";
protected render(): TemplateResult {
if (!this.addon) {
return html`
@@ -27,31 +23,16 @@ class HassioAddonLogDashboard extends LitElement {
`;
}
return html`
<div class="search">
<search-input
@value-changed=${this._filterChanged}
.hass=${this.hass}
.filter=${this._filter}
.label=${this.hass.localize("ui.panel.config.logs.search")}
></search-input>
</div>
<div class="content">
<error-log-card
<hassio-addon-logs
.hass=${this.hass}
.header=${this.addon.name}
.provider=${this.addon.slug}
show
.filter=${this._filter}
>
</error-log-card>
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-logs>
</div>
`;
}
private async _filterChanged(ev) {
this._filter = ev.detail.value;
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -60,21 +41,7 @@ class HassioAddonLogDashboard extends LitElement {
.content {
margin: auto;
padding: 8px;
}
.search {
position: sticky;
top: 0;
z-index: 2;
}
search-input {
display: block;
--mdc-text-field-fill-color: var(--sidebar-background-color);
--mdc-text-field-idle-line-color: var(--divider-color);
}
@media all and (max-width: 870px) {
:host {
--error-log-card-height: calc(100vh - 304px);
}
max-width: 1024px;
}
`,
];

View File

@@ -0,0 +1,90 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-ansi-to-html";
import "../../../../src/components/ha-card";
import {
fetchHassioAddonLogs,
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@customElement("hassio-addon-logs")
class HassioAddonLogs extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@state() private _error?: string;
@state() private _content?: string;
public async connectedCallback(): Promise<void> {
super.connectedCallback();
await this._loadData();
}
protected render(): TemplateResult {
return html`
<h1>${this.addon.name}</h1>
<ha-card outlined>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="card-content">
${this._content
? html`<ha-ansi-to-html
.content=${this._content}
></ha-ansi-to-html>`
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>
${this.supervisor.localize("common.refresh")}
</mwc-button>
</div>
</ha-card>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
hassioStyle,
css`
:host,
ha-card {
display: block;
}
`,
];
}
private async _loadData(): Promise<void> {
this._error = undefined;
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err: any) {
this._error = this.supervisor.localize("addon.logs.get_logs", {
error: extractApiErrorMessage(err),
});
}
}
private async _refresh(): Promise<void> {
await this._loadData();
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-addon-logs": HassioAddonLogs;
}
}

View File

@@ -48,7 +48,6 @@ import { showHassioBackupDialog } from "../dialogs/backup/show-dialog-hassio-bac
import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hassio-create-backup";
import { supervisorTabs } from "../hassio-tabs";
import { hassioStyle } from "../resources/hassio-style";
import "../../../src/layouts/hass-loading-screen";
type BackupItem = HassioBackup & {
secondary: string;
@@ -70,8 +69,6 @@ export class HassioBackups extends LitElement {
@state() private _backups?: HassioBackup[] = [];
@state() private _isLoading = false;
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@@ -80,10 +77,15 @@ export class HassioBackups extends LitElement {
public connectedCallback(): void {
super.connectedCallback();
if (this.hass && this._firstUpdatedCalled) {
this.fetchBackups();
this.refreshData();
}
}
public async refreshData() {
await reloadHassioBackups(this.hass);
await this.fetchBackups();
}
private _computeBackupContent = (backup: HassioBackup): string => {
if (backup.type === "full") {
return this.supervisor.localize("backup.full_backup");
@@ -113,7 +115,7 @@ export class HassioBackups extends LitElement {
protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
if (this.hass && this.isConnected) {
this.fetchBackups();
this.refreshData();
}
this._firstUpdatedCalled = true;
}
@@ -125,13 +127,14 @@ export class HassioBackups extends LitElement {
main: true,
sortable: true,
filterable: true,
flex: 2,
grows: true,
template: (backup) =>
html`${backup.name || backup.slug}
<div class="secondary">${backup.secondary}</div>`,
},
size: {
title: this.supervisor.localize("backup.size"),
width: "15%",
hidden: narrow,
filterable: true,
sortable: true,
@@ -139,6 +142,7 @@ export class HassioBackups extends LitElement {
},
location: {
title: this.supervisor.localize("backup.location"),
width: "15%",
hidden: narrow,
filterable: true,
sortable: true,
@@ -147,6 +151,7 @@ export class HassioBackups extends LitElement {
},
date: {
title: this.supervisor.localize("backup.created"),
width: "15%",
direction: "desc",
hidden: narrow,
filterable: true,
@@ -173,13 +178,6 @@ export class HassioBackups extends LitElement {
if (!this.supervisor) {
return nothing;
}
if (this._isLoading) {
return html`<hass-loading-screen
.message=${this.supervisor.localize("backup.loading_backups")}
></hass-loading-screen>`;
}
return html`
<hass-tabs-subpage-data-table
.tabs=${atLeastVersion(this.hass.config.version, 2022, 5)
@@ -286,7 +284,7 @@ export class HassioBackups extends LitElement {
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
this.fetchBackups();
this.refreshData();
break;
case 1:
showHassioBackupLocationDialog(this, { supervisor: this.supervisor });
@@ -311,15 +309,13 @@ export class HassioBackups extends LitElement {
supervisor: this.supervisor,
onDelete: () => this.fetchBackups(),
}),
reloadBackup: () => this.fetchBackups(),
reloadBackup: () => this.refreshData(),
});
}
private async fetchBackups() {
this._isLoading = true;
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
this._isLoading = false;
}
private async _deleteSelected() {
@@ -346,7 +342,8 @@ export class HassioBackups extends LitElement {
});
return;
}
await this.fetchBackups();
await reloadHassioBackups(this.hass);
this._backups = await fetchHassioBackups(this.hass);
this._dataTable.clearSelection();
}

View File

@@ -1,8 +1,6 @@
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
import { stripDiacritics } from "../../../src/common/string/strip-diacritics";
import type { IFuseOptions } from "fuse.js";
import { StoreAddon } from "../../../src/data/supervisor/store";
import { getStripDiacriticsFn } from "../../../src/util/fuse";
export function filterAndSort(addons: StoreAddon[], filter: string) {
const options: IFuseOptions<StoreAddon> = {
@@ -10,8 +8,7 @@ export function filterAndSort(addons: StoreAddon[], filter: string) {
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
getFn: getStripDiacriticsFn,
};
const fuse = new Fuse(addons, options);
return fuse.search(stripDiacritics(filter)).map((result) => result.item);
return fuse.search(filter).map((result) => result.item);
}

View File

@@ -15,7 +15,6 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
import "../../../src/components/ha-checkbox";
import "../../../src/components/ha-formfield";
import "../../../src/components/ha-textfield";
import "../../../src/components/ha-password-field";
import "../../../src/components/ha-radio";
import type { HaRadio } from "../../../src/components/ha-radio";
import {
@@ -262,21 +261,23 @@ export class SupervisorBackupContent extends LitElement {
: ""}
${this.backupHasPassword
? html`
<ha-password-field
<ha-textfield
.label=${this._localize("password")}
type="password"
name="backupPassword"
.value=${this.backupPassword}
@change=${this._handleTextValueChanged}
>
</ha-password-field>
</ha-textfield>
${!this.backup
? html`<ha-password-field
? html`<ha-textfield
.label=${this._localize("confirm_password")}
type="password"
name="confirmBackupPassword"
.value=${this.confirmBackupPassword}
@change=${this._handleTextValueChanged}
>
</ha-password-field>`
</ha-textfield>`
: ""}
`
: ""}

View File

@@ -13,12 +13,10 @@ import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-password-field";
import "../../../../src/components/ha-radio";
import "../../../../src/components/ha-textfield";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import {
AccessPoints,
@@ -36,6 +34,7 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
import type { HaTextField } from "../../../../src/components/ha-textfield";
const IP_VERSIONS = ["ipv4", "ipv6"];
@@ -247,8 +246,9 @@ export class DialogHassioNetwork
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<ha-password-field
<ha-textfield
class="flex-auto"
type="password"
id="psk"
.label=${this.supervisor.localize(
"dialog.network.wifi_password"
@@ -256,7 +256,7 @@ export class DialogHassioNetwork
version="wifi"
@change=${this._handleInputValueChangedWifi}
>
</ha-password-field>
</ha-textfield>
`
: ""}
`

View File

@@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
import type { HaTextField } from "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-textfield";
import "../../../../src/components/ha-md-list";
import "../../../../src/components/ha-md-list-item";
import "../../../../src/components/ha-list-new";
import "../../../../src/components/ha-list-item-new";
@customElement("dialog-hassio-repositories")
class HassioRepositoriesDialog extends LitElement {
@@ -66,8 +66,7 @@ class HassioRepositoriesDialog extends LitElement {
repo.slug !== "core" && // The core add-ons repository
repo.slug !== "local" && // Locally managed add-ons
repo.slug !== "a0d7b954" && // Home Assistant Community Add-ons
repo.slug !== "5c53de3b" && // The ESPHome repository
repo.slug !== "d5369777" // Music Assistant repository
repo.slug !== "5c53de3b" // The ESPHome repository
)
.sort((a, b) =>
caseInsensitiveStringCompare(a.name, b.name, this.hass.locale.language)
@@ -107,11 +106,11 @@ class HassioRepositoriesDialog extends LitElement {
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-md-list>
<ha-list-new>
${repositories.length
? repositories.map(
(repo) => html`
<ha-md-list-item class="option">
<ha-list-item-new class="option">
${repo.name}
<div slot="supporting-text">
<div>${repo.maintainer}</div>
@@ -142,11 +141,11 @@ class HassioRepositoriesDialog extends LitElement {
)}
</simple-tooltip>
</div>
</ha-md-list-item>
</ha-list-item-new>
`
)
: html`<ha-md-list-item> No repositories </ha-md-list-item>`}
</ha-md-list>
: html`<ha-list-item-new> No repositories </ha-list-item-new>`}
</ha-list-new>
<div class="layout horizontal bottom">
<ha-textfield
class="flex-auto"
@@ -209,7 +208,7 @@ class HassioRepositoriesDialog extends LitElement {
div.delete ha-icon-button {
color: var(--error-color);
}
ha-md-list-item {
ha-list-item-new {
position: relative;
}
`,

Some files were not shown because too many files have changed in this diff Show More