mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-29 12:31:52 +00:00
Compare commits
193 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c448bbc3b4 | |||
| 4dcf26236e | |||
| a0e8d69243 | |||
| 33cd9bf516 | |||
| 0132797f2f | |||
| 7e2db0aa4e | |||
| cc1d50491b | |||
| 461b86a04b | |||
| 9a3a7c28f4 | |||
| 1c9d0200ca | |||
| 0037cd2e69 | |||
| 028ae061da | |||
| 2e47763ecc | |||
| 924e4a45d0 | |||
| 8361b9553b | |||
| e52be20fba | |||
| da12233ade | |||
| 57500f6c97 | |||
| 199e17d0b1 | |||
| 3b91343082 | |||
| 1753c9163c | |||
| 89e5953e89 | |||
| 5bfd25c8c6 | |||
| e555b24f50 | |||
| 14db37459f | |||
| 1d9779d47c | |||
| 3dedbc5457 | |||
| facb3266c6 | |||
| 01fe5dd2f7 | |||
| 9b22b1e499 | |||
| 4bc8818145 | |||
| 48ef8c86c2 | |||
| 89f359a52f | |||
| 13b8160d74 | |||
| f1c16d6674 | |||
| 76a088e177 | |||
| 630d8c3bb6 | |||
| f0e959319e | |||
| d0c4475724 | |||
| 99935f1e59 | |||
| fbb43821ba | |||
| c7f5c6c1d1 | |||
| d26f1fa371 | |||
| c3718ff7dd | |||
| d63493a859 | |||
| a72183851a | |||
| 40b2387667 | |||
| d814aa36a7 | |||
| 58a58906e7 | |||
| ba99d1a10d | |||
| efe97e8f51 | |||
| 5ec23bb7ab | |||
| 9b4d01ab75 | |||
| 40191a88d4 | |||
| a19477d179 | |||
| bf98a78f3d | |||
| ba4c2fc1bd | |||
| b56e9ef028 | |||
| dbbd34c520 | |||
| ccb69dbdfa | |||
| 11e555ef6f | |||
| 61e17395c9 | |||
| 733ce3b6b8 | |||
| 375f143199 | |||
| 2419f35eb9 | |||
| 21867c3576 | |||
| 28853b28bc | |||
| e2f27568a5 | |||
| 98b2b796b0 | |||
| b8f3fcf00b | |||
| d3fda9a821 | |||
| 19e69dc13e | |||
| 5fa7cd9fa9 | |||
| a78c00fb41 | |||
| edc2a03d1c | |||
| 174f8f5823 | |||
| 9fbc94e8d8 | |||
| 6aff35196d | |||
| eceed4ed74 | |||
| 7428731eac | |||
| 89b07ea0ae | |||
| d16daf0fd9 | |||
| 211ab4eea8 | |||
| dbd53f8d14 | |||
| 07fc9b98cc | |||
| 33582c0448 | |||
| 611202c905 | |||
| e553f35a68 | |||
| 673649a603 | |||
| c4ed743370 | |||
| 682fa0d3eb | |||
| 30f34eee22 | |||
| eab76bf85b | |||
| bcf405bf9d | |||
| 3c4b0d4a74 | |||
| fb9bd0eb7d | |||
| 7e2dc04123 | |||
| 54ec37994c | |||
| 4a5935ee36 | |||
| 01b9a07320 | |||
| 0fcf0dcd18 | |||
| 80481f142a | |||
| 2be08ce7ab | |||
| 37eb5af3d4 | |||
| 8c8151be92 | |||
| baf31d1c1e | |||
| af2250835a | |||
| 6f2a759ba3 | |||
| 5065901196 | |||
| 41b59e6e11 | |||
| 43afdaadc6 | |||
| 83c5151792 | |||
| 0880ab67c6 | |||
| c0b2143c7c | |||
| c1de162c99 | |||
| a7ef8aba68 | |||
| 3ee4c11a99 | |||
| 990ae10dc2 | |||
| 52b2fd046b | |||
| 9f41f80a91 | |||
| eec4a91ad8 | |||
| 7c51001c3c | |||
| a4ea4b1f5f | |||
| 19fc37539e | |||
| ce7acb0feb | |||
| 105b7678b8 | |||
| b67575586e | |||
| 3dc6898673 | |||
| a73754c1b5 | |||
| 1ebf1c00d6 | |||
| 7dac7d757e | |||
| b1f3192b95 | |||
| 16984d18bb | |||
| e603893d77 | |||
| a7998b30c6 | |||
| 3277a4e8c3 | |||
| 7e769d0e14 | |||
| 713e0579f8 | |||
| 6e130cc020 | |||
| eb036a12d9 | |||
| 534d1f5055 | |||
| cbef909657 | |||
| 874f3b32b3 | |||
| 2fd017cf73 | |||
| 5740b018a7 | |||
| 288bf6805a | |||
| 02d37a369a | |||
| 1d316c3258 | |||
| a56ce62f1a | |||
| c268f42851 | |||
| 7251e802ab | |||
| 5b1a2d10c2 | |||
| 2dd7f292b1 | |||
| 213c53e307 | |||
| ce07dfd8ac | |||
| c1dba462e8 | |||
| 47f0d74812 | |||
| ce80285f8d | |||
| d2dd1a43dd | |||
| 12d73fe90d | |||
| c2741638b2 | |||
| 4a7fb3d509 | |||
| f6ff652ca4 | |||
| 6165cb0f83 | |||
| 1f361b7b10 | |||
| 5269ff978b | |||
| 55595493a9 | |||
| ad3ff0aba7 | |||
| ce48546cef | |||
| 35b3bc995e | |||
| 63f60019d1 | |||
| 0d741b6275 | |||
| 0df9080bbb | |||
| ddcf89e6a2 | |||
| 5de225d5d4 | |||
| 5cddb482f1 | |||
| c000d724de | |||
| 504055f331 | |||
| 7f6880f40e | |||
| 02e4e3c892 | |||
| b5b1849ab3 | |||
| 0e10c81025 | |||
| cce7ad449a | |||
| d437dd5919 | |||
| f1980730d2 | |||
| 47773e9cae | |||
| 60969b0916 | |||
| ecc7925d03 | |||
| 6d3010dcc7 | |||
| 0164bafbf1 | |||
| 98a64e3114 | |||
| 6ef3d091e1 | |||
| b612c0e0d6 |
+3
-1
@@ -75,6 +75,7 @@
|
||||
"object-curly-newline": 0,
|
||||
"default-case": 0,
|
||||
"wc/no-self-class": 0,
|
||||
"no-shadow": 0,
|
||||
"@typescript-eslint/camelcase": 0,
|
||||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"@typescript-eslint/no-use-before-define": 0,
|
||||
@@ -82,7 +83,8 @@
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/no-unused-vars": 0,
|
||||
"@typescript-eslint/explicit-function-return-type": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/no-shadow": ["error"]
|
||||
},
|
||||
"plugins": ["disable", "import", "lit", "prettier", "@typescript-eslint"],
|
||||
"processor": "disable/disable"
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
---
|
||||
name: Request a feature for the UI, Frontend or Lovelace
|
||||
about: Request an new feature for the Home Assistant frontend.
|
||||
labels: feature request
|
||||
---
|
||||
|
||||
<!--
|
||||
DO NOT DELETE ANY TEXT from this template!
|
||||
Otherwise, your request may be closed without comment.
|
||||
-->
|
||||
|
||||
## The request
|
||||
|
||||
<!--
|
||||
Describe to our maintainers, the feature you would like to be added.
|
||||
Please be clear and concise and, if possible, provide a screenshot or mockup.
|
||||
-->
|
||||
|
||||
## The alternatives
|
||||
|
||||
<!--
|
||||
Are you currently using, or have you considered alternatives?
|
||||
If so, could you please describe those?
|
||||
-->
|
||||
|
||||
## Additional information
|
||||
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Request a feature for the UI, Frontend or Lovelace
|
||||
url: https://github.com/home-assistant/frontend/discussions/category_choices
|
||||
about: Request an new feature for the Home Assistant frontend.
|
||||
- name: Report a bug that is NOT related to the UI, Frontend or Lovelace
|
||||
url: https://github.com/home-assistant/core/issues
|
||||
about: This is the issue tracker for our frontend. Please report other issues with the backend repository.
|
||||
|
||||
@@ -26,4 +26,4 @@ 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 variation of devices.
|
||||
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
|
||||
|
||||
@@ -54,7 +54,10 @@ module.exports.babelOptions = ({ latestBuild }) => ({
|
||||
presets: [
|
||||
!latestBuild && [
|
||||
require("@babel/preset-env").default,
|
||||
{ modules: false, useBuiltIns: "entry", corejs: 3 },
|
||||
{
|
||||
useBuiltIns: "entry",
|
||||
corejs: "3.6",
|
||||
},
|
||||
],
|
||||
require("@babel/preset-typescript").default,
|
||||
].filter(Boolean),
|
||||
@@ -66,7 +69,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({
|
||||
],
|
||||
// Only support the syntax, Webpack will handle it.
|
||||
"@babel/plugin-syntax-import-meta",
|
||||
"@babel/syntax-dynamic-import",
|
||||
"@babel/plugin-syntax-dynamic-import",
|
||||
"@babel/plugin-proposal-optional-chaining",
|
||||
"@babel/plugin-proposal-nullish-coalescing-operator",
|
||||
[
|
||||
|
||||
@@ -7,7 +7,6 @@ const gulp = require("gulp");
|
||||
const fs = require("fs");
|
||||
const foreach = require("gulp-foreach");
|
||||
const merge = require("gulp-merge-json");
|
||||
const minify = require("gulp-jsonminify");
|
||||
const rename = require("gulp-rename");
|
||||
const transform = require("gulp-json-transform");
|
||||
const { mapFiles } = require("../util");
|
||||
@@ -301,7 +300,6 @@ gulp.task("build-flattened-translations", function () {
|
||||
return flatten(data);
|
||||
})
|
||||
)
|
||||
.pipe(minify())
|
||||
.pipe(
|
||||
rename((filePath) => {
|
||||
if (filePath.dirname === "core") {
|
||||
|
||||
@@ -29,7 +29,7 @@ const createWebpackConfig = ({
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$|\.ts$/,
|
||||
test: /\.m?js$|\.ts$/,
|
||||
exclude: bundle.babelExclude(),
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
@@ -45,10 +45,8 @@ const createWebpackConfig = ({
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
extractComments: true,
|
||||
sourceMap: true,
|
||||
terserOptions: bundle.terserOptions(latestBuild),
|
||||
}),
|
||||
],
|
||||
@@ -97,6 +95,15 @@ const createWebpackConfig = ({
|
||||
new RegExp(bundle.emptyPackages({ latestBuild }).join("|")),
|
||||
path.resolve(paths.polymer_dir, "src/util/empty.js")
|
||||
),
|
||||
// We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one
|
||||
new webpack.NormalModuleReplacementPlugin(
|
||||
new RegExp(
|
||||
require.resolve(
|
||||
"lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js"
|
||||
)
|
||||
),
|
||||
path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js")
|
||||
),
|
||||
],
|
||||
resolve: {
|
||||
extensions: [".ts", ".js", ".json"],
|
||||
@@ -108,6 +115,22 @@ const createWebpackConfig = ({
|
||||
}
|
||||
return `${chunk.name}.${chunk.hash.substr(0, 8)}.js`;
|
||||
},
|
||||
environment: {
|
||||
// The environment supports arrow functions ('() => { ... }').
|
||||
arrowFunction: latestBuild,
|
||||
// The environment supports BigInt as literal (123n).
|
||||
bigIntLiteral: false,
|
||||
// The environment supports const and let for variable declarations.
|
||||
const: latestBuild,
|
||||
// The environment supports destructuring ('{ a, b } = obj').
|
||||
destructuring: latestBuild,
|
||||
// The environment supports an async import() function to import EcmaScript modules.
|
||||
dynamicImport: latestBuild,
|
||||
// The environment supports 'for of' iteration ('for (const x of array) { ... }').
|
||||
forOf: latestBuild,
|
||||
// The environment supports ECMAScript Module syntax to import ECMAScript modules (import ... from '...').
|
||||
module: latestBuild,
|
||||
},
|
||||
chunkFilename:
|
||||
isProdBuild && !isStatsBuild
|
||||
? "chunk.[chunkhash].js"
|
||||
|
||||
@@ -30,7 +30,7 @@ class HcLayout extends LitElement {
|
||||
<ha-card>
|
||||
<div class="layout">
|
||||
<img class="hero" src="/images/google-nest-hub.png" />
|
||||
<div class="card-header">
|
||||
<h1 class="card-header">
|
||||
Home Assistant Cast${this.subtitle ? ` – ${this.subtitle}` : ""}
|
||||
${this.auth
|
||||
? html`
|
||||
@@ -44,7 +44,7 @@ class HcLayout extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
</h1>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
@@ -7,205 +7,183 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({
|
||||
cards: [
|
||||
{ type: "custom:ha-demo-card" },
|
||||
{
|
||||
type: "grid",
|
||||
columns: 4,
|
||||
cards: [
|
||||
{
|
||||
cards: [
|
||||
image: "/assets/teachingbirds/isa_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
entity: "sensor.presence_isa",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/Stefan_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
entity: "sensor.presence_stefan",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/background_square.png",
|
||||
elements: [
|
||||
{
|
||||
image: "/assets/teachingbirds/isa_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
state_image: {
|
||||
on: "/assets/teachingbirds/radiator_on.jpg",
|
||||
off: "/assets/teachingbirds/radiator_off.jpg",
|
||||
},
|
||||
type: "image",
|
||||
style: {
|
||||
width: "100%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
entity: "sensor.presence_isa",
|
||||
entity: "switch.stefan_radiator_3",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/Stefan_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
style: {
|
||||
top: "90%",
|
||||
left: "50%",
|
||||
},
|
||||
entity: "sensor.presence_stefan",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/background_square.png",
|
||||
elements: [
|
||||
{
|
||||
state_image: {
|
||||
on: "/assets/teachingbirds/radiator_on.jpg",
|
||||
off: "/assets/teachingbirds/radiator_off.jpg",
|
||||
},
|
||||
type: "image",
|
||||
style: {
|
||||
width: "100%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
entity: "switch.stefan_radiator_3",
|
||||
},
|
||||
{
|
||||
style: {
|
||||
top: "90%",
|
||||
left: "50%",
|
||||
},
|
||||
type: "state-label",
|
||||
entity: "sensor.temperature_stefan",
|
||||
},
|
||||
],
|
||||
type: "picture-elements",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/background_square.png",
|
||||
elements: [
|
||||
{
|
||||
style: {
|
||||
"--mdc-icon-size": "100%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
type: "icon",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/lovelace/home_info",
|
||||
},
|
||||
icon: "mdi:car",
|
||||
},
|
||||
],
|
||||
type: "picture-elements",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
name: "Alarm",
|
||||
image: "/assets/teachingbirds/House_square.jpg",
|
||||
entity: "alarm_control_panel.house",
|
||||
},
|
||||
{
|
||||
name: "Roomba",
|
||||
image: "/assets/teachingbirds/roomba_square.jpg",
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
"Not Today": "/assets/teachingbirds/roomba_bw_square.jpg",
|
||||
},
|
||||
entity: "input_select.roomba_mode",
|
||||
},
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
Mail: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
"Package and mail":
|
||||
"/assets/teachingbirds/mailbox_square.jpg",
|
||||
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
|
||||
Package: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
},
|
||||
entity: "sensor.mailbox",
|
||||
},
|
||||
{
|
||||
show_name: false,
|
||||
state_image: {
|
||||
"Put out": "/assets/teachingbirds/trash_square.jpg",
|
||||
"Take in": "/assets/teachingbirds/trash_square.jpg",
|
||||
},
|
||||
type: "picture-entity",
|
||||
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
|
||||
entity: "sensor.trash_status",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/washer_square.jpg",
|
||||
Running: "/assets/teachingbirds/laundry_running_square.jpg",
|
||||
Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg",
|
||||
},
|
||||
entity: "input_select.washing_machine_status",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
name: "Washer",
|
||||
},
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/dryer_square.jpg",
|
||||
Running: "/assets/teachingbirds/clothes_drying_square.jpg",
|
||||
Clean: "/assets/teachingbirds/folded_clothes_square.jpg",
|
||||
},
|
||||
entity: "input_select.dryer_status",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
name: "Dryer",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/guests_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
entity: "input_boolean.guest_mode",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/cleaning_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
entity: "input_boolean.cleaning_day",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
},
|
||||
],
|
||||
type: "vertical-stack",
|
||||
},
|
||||
{
|
||||
type: "vertical-stack",
|
||||
cards: [
|
||||
{
|
||||
cards: [
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_bedroom",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
name: "S's room",
|
||||
type: "state-label",
|
||||
entity: "sensor.temperature_stefan",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
type: "picture-elements",
|
||||
},
|
||||
{
|
||||
cards: [
|
||||
image: "/assets/teachingbirds/background_square.png",
|
||||
elements: [
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_passage",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
name: "Laundry",
|
||||
entity: "sensor.temperature_downstairs_bathroom",
|
||||
style: {
|
||||
"--mdc-icon-size": "100%",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
},
|
||||
type: "icon",
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "/lovelace/home_info",
|
||||
},
|
||||
icon: "mdi:car",
|
||||
},
|
||||
],
|
||||
type: "horizontal-stack",
|
||||
type: "picture-elements",
|
||||
},
|
||||
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
name: "Alarm",
|
||||
image: "/assets/teachingbirds/House_square.jpg",
|
||||
entity: "alarm_control_panel.house",
|
||||
},
|
||||
{
|
||||
name: "Roomba",
|
||||
image: "/assets/teachingbirds/roomba_square.jpg",
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
"Not Today": "/assets/teachingbirds/roomba_bw_square.jpg",
|
||||
},
|
||||
entity: "input_select.roomba_mode",
|
||||
},
|
||||
{
|
||||
show_name: false,
|
||||
type: "picture-entity",
|
||||
state_image: {
|
||||
Mail: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
"Package and mail": "/assets/teachingbirds/mailbox_square.jpg",
|
||||
Empty: "/assets/teachingbirds/mailbox_bw_square.jpg",
|
||||
Package: "/assets/teachingbirds/mailbox_square.jpg",
|
||||
},
|
||||
entity: "sensor.mailbox",
|
||||
},
|
||||
{
|
||||
show_name: false,
|
||||
state_image: {
|
||||
"Put out": "/assets/teachingbirds/trash_square.jpg",
|
||||
"Take in": "/assets/teachingbirds/trash_square.jpg",
|
||||
},
|
||||
type: "picture-entity",
|
||||
image: "/assets/teachingbirds/trash_bear_bw_square.jpg",
|
||||
entity: "sensor.trash_status",
|
||||
},
|
||||
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/washer_square.jpg",
|
||||
Running: "/assets/teachingbirds/laundry_running_square.jpg",
|
||||
Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg",
|
||||
},
|
||||
entity: "input_select.washing_machine_status",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
name: "Washer",
|
||||
},
|
||||
{
|
||||
state_image: {
|
||||
Idle: "/assets/teachingbirds/dryer_square.jpg",
|
||||
Running: "/assets/teachingbirds/clothes_drying_square.jpg",
|
||||
Clean: "/assets/teachingbirds/folded_clothes_square.jpg",
|
||||
},
|
||||
entity: "input_select.dryer_status",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
name: "Dryer",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/guests_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
entity: "input_boolean.guest_mode",
|
||||
},
|
||||
{
|
||||
image: "/assets/teachingbirds/cleaning_square.jpg",
|
||||
type: "picture-entity",
|
||||
show_name: false,
|
||||
tap_action: {
|
||||
action: "toggle",
|
||||
},
|
||||
entity: "input_boolean.cleaning_day",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "grid",
|
||||
columns: 2,
|
||||
cards: [
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_bedroom",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
name: "S's room",
|
||||
entity: "sensor.temperature_stefan",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
entity: "sensor.temperature_passage",
|
||||
},
|
||||
{
|
||||
graph: "line",
|
||||
type: "sensor",
|
||||
name: "Laundry",
|
||||
entity: "sensor.temperature_downstairs_bathroom",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,4 +6,11 @@ export const mockTemplate = (hass: MockHomeAssistant) => {
|
||||
body: { message: "Template dev tool does not work in the demo." },
|
||||
})
|
||||
);
|
||||
hass.mockWS("render_template", (msg, onChange) => {
|
||||
onChange!({
|
||||
result: msg.template,
|
||||
listeners: { all: false, domains: [], entities: [], time: false },
|
||||
});
|
||||
return () => {};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -5,11 +5,16 @@ import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../src/components/ha-switch";
|
||||
import "../../../src/components/ha-formfield";
|
||||
import "./demo-card";
|
||||
import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
|
||||
|
||||
class DemoCards extends PolymerElement {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
#container {
|
||||
min-height: calc(100vh - 128px);
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -24,6 +29,9 @@ class DemoCards extends PolymerElement {
|
||||
.filters {
|
||||
margin-left: 60px;
|
||||
}
|
||||
ha-formfield {
|
||||
margin-right: 16px;
|
||||
}
|
||||
</style>
|
||||
<app-toolbar>
|
||||
<div class="filters">
|
||||
@@ -31,16 +39,21 @@ class DemoCards extends PolymerElement {
|
||||
<ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled">
|
||||
</ha-switch>
|
||||
</ha-formfield>
|
||||
<ha-formfield label="Dark theme">
|
||||
<ha-switch on-change="_darkThemeToggled"> </ha-switch>
|
||||
</ha-formfield>
|
||||
</div>
|
||||
</app-toolbar>
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[configs]]">
|
||||
<demo-card
|
||||
config="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-card>
|
||||
</template>
|
||||
<div id="container">
|
||||
<div class="cards">
|
||||
<template is="dom-repeat" items="[[configs]]">
|
||||
<demo-card
|
||||
config="[[item]]"
|
||||
show-config="[[_showConfig]]"
|
||||
hass="[[hass]]"
|
||||
></demo-card>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -59,6 +72,12 @@ class DemoCards extends PolymerElement {
|
||||
_showConfigToggled(ev) {
|
||||
this._showConfig = ev.target.checked;
|
||||
}
|
||||
|
||||
_darkThemeToggled(ev) {
|
||||
applyThemesOnElement(this.$.container, { themes: {} }, "default", {
|
||||
dark: ev.target.checked,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-cards", DemoCards);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/state-summary/state-card-content";
|
||||
import "./more-info-content";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
|
||||
class DemoMoreInfo extends PolymerElement {
|
||||
static get template() {
|
||||
@@ -16,15 +16,12 @@ class DemoMoreInfo extends PolymerElement {
|
||||
|
||||
ha-card {
|
||||
width: 333px;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
state-card-content {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
more-info-content {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
pre {
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { property, PropertyValues, UpdatingElement } from "lit-element";
|
||||
import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater";
|
||||
import { stateMoreInfoType } from "../../../src/dialogs/more-info/state_more_info_control";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-automation";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-camera";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-climate";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-configurator";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-counter";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-cover";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-default";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-fan";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-group";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-humidifier";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-input_datetime";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-light";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-lock";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-media_player";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-person";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-script";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-sun";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-timer";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-vacuum";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-water_heater";
|
||||
import "../../../src/dialogs/more-info/controls/more-info-weather";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
class MoreInfoContent extends UpdatingElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public stateObj?: HassEntity;
|
||||
|
||||
private _detachedChild?: ChildNode;
|
||||
|
||||
protected firstUpdated(): void {
|
||||
this.style.position = "relative";
|
||||
this.style.display = "block";
|
||||
}
|
||||
|
||||
// This is not a lit element, but an updating element, so we implement update
|
||||
protected update(changedProps: PropertyValues): void {
|
||||
super.update(changedProps);
|
||||
const stateObj = this.stateObj;
|
||||
const hass = this.hass;
|
||||
|
||||
if (!stateObj || !hass) {
|
||||
if (this.lastChild) {
|
||||
this._detachedChild = this.lastChild;
|
||||
// Detach child to prevent it from doing work.
|
||||
this.removeChild(this.lastChild);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._detachedChild) {
|
||||
this.appendChild(this._detachedChild);
|
||||
this._detachedChild = undefined;
|
||||
}
|
||||
|
||||
const moreInfoType =
|
||||
stateObj.attributes && "custom_ui_more_info" in stateObj.attributes
|
||||
? stateObj.attributes.custom_ui_more_info
|
||||
: "more-info-" + stateMoreInfoType(stateObj);
|
||||
|
||||
dynamicContentUpdater(this, moreInfoType.toUpperCase(), {
|
||||
hass,
|
||||
stateObj,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("more-info-content", MoreInfoContent);
|
||||
@@ -15,6 +15,10 @@ const ENTITIES = [
|
||||
getEntity("alarm_control_panel", "unavailable", "unavailable", {
|
||||
friendly_name: "Alarm",
|
||||
}),
|
||||
getEntity("alarm_control_panel", "alarm_code", "disarmed", {
|
||||
friendly_name: "Alarm",
|
||||
code_format: "number",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -30,7 +34,14 @@ const CONFIGS = [
|
||||
config: `
|
||||
- type: alarm-panel
|
||||
entity: alarm_control_panel.alarm_armed
|
||||
title: My Alarm
|
||||
name: My Alarm
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Code Example",
|
||||
config: `
|
||||
- type: alarm-panel
|
||||
entity: alarm_control_panel.alarm_code
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -83,8 +94,12 @@ class DemoAlarmPanelEntity extends PolymerElement {
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
this._setupDemo();
|
||||
}
|
||||
|
||||
private async _setupDemo() {
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.updateTranslations(null, "en");
|
||||
await hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,4 @@ class DemoButtonEntity extends PolymerElement {
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-button-card", DemoButtonEntity);
|
||||
customElements.define("demo-hui-entity-button-card", DemoButtonEntity);
|
||||
|
||||
@@ -8,6 +8,7 @@ import "../components/demo-cards";
|
||||
const ENTITIES = [
|
||||
getEntity("sensor", "brightness", "12", {}),
|
||||
getEntity("plant", "bonsai", "ok", {}),
|
||||
getEntity("sensor", "not_working", "unavailable", {}),
|
||||
getEntity("sensor", "outside_humidity", "54", {
|
||||
unit_of_measurement: "%",
|
||||
}),
|
||||
@@ -74,6 +75,13 @@ const CONFIGS = [
|
||||
entity: plant.bonsai
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Unavailable entity",
|
||||
config: `
|
||||
- type: gauge
|
||||
entity: sensor.not_working
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoGaugeEntity extends PolymerElement {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { mockTemplate } from "../../../demo/src/stubs/template";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -254,7 +256,7 @@ const CONFIGS = [
|
||||
|
||||
class DemoMarkdown extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <demo-cards configs="[[_configs]]"></demo-cards> `;
|
||||
return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
@@ -265,6 +267,12 @@ class DemoMarkdown extends PolymerElement {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
const hass = provideHass(this.$.demos);
|
||||
mockTemplate(hass);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("demo-hui-markdown-card", DemoMarkdown);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { mockHistory } from "../../../demo/src/stubs/history";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-cards";
|
||||
@@ -36,6 +37,10 @@ const ENTITIES = [
|
||||
battery: 71,
|
||||
friendly_name: "Home Boy",
|
||||
}),
|
||||
getEntity("sensor", "illumination", "23", {
|
||||
friendly_name: "Illumination",
|
||||
unit_of_measurement: "lx",
|
||||
}),
|
||||
];
|
||||
|
||||
const CONFIGS = [
|
||||
@@ -89,6 +94,42 @@ const CONFIGS = [
|
||||
entity: light.bed_light
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Default Grid",
|
||||
config: `
|
||||
- type: grid
|
||||
cards:
|
||||
- type: entity
|
||||
entity: light.kitchen_lights
|
||||
- type: entity
|
||||
entity: light.bed_light
|
||||
- type: entity
|
||||
entity: device_tracker.demo_paulus
|
||||
- type: sensor
|
||||
entity: sensor.illumination
|
||||
graph: line
|
||||
- type: entity
|
||||
entity: device_tracker.demo_anne_therese
|
||||
`,
|
||||
},
|
||||
{
|
||||
heading: "Non-square Grid with 2 columns",
|
||||
config: `
|
||||
- type: grid
|
||||
columns: 2
|
||||
square: false
|
||||
cards:
|
||||
- type: entity
|
||||
entity: light.kitchen_lights
|
||||
- type: entity
|
||||
entity: light.bed_light
|
||||
- type: entity
|
||||
entity: device_tracker.demo_paulus
|
||||
- type: sensor
|
||||
entity: sensor.illumination
|
||||
graph: line
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
class DemoStack extends PolymerElement {
|
||||
@@ -110,6 +151,7 @@ class DemoStack extends PolymerElement {
|
||||
const hass = provideHass(this.$.demos);
|
||||
hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
mockHistory(hass);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SUPPORT_BRIGHTNESS } from "../../../src/data/light";
|
||||
import { getEntity } from "../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||
import "../components/demo-more-infos";
|
||||
import "../components/more-info-content";
|
||||
import "../../../src/dialogs/more-info/more-info-content";
|
||||
|
||||
const ENTITIES = [
|
||||
getEntity("light", "bed_light", "on", {
|
||||
@@ -40,8 +40,12 @@ class DemoMoreInfoLight extends PolymerElement {
|
||||
|
||||
public ready() {
|
||||
super.ready();
|
||||
this._setupDemo();
|
||||
}
|
||||
|
||||
private async _setupDemo() {
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
await hass.updateTranslations(null, "en");
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,9 @@ import { hassioStyle } from "../resources/hassio-style";
|
||||
class HassioAddonRepositoryEl extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public repo!: HassioAddonRepository;
|
||||
@property({ attribute: false }) public repo!: HassioAddonRepository;
|
||||
|
||||
@property() public addons!: HassioAddonInfo[];
|
||||
@property({ attribute: false }) public addons!: HassioAddonInfo[];
|
||||
|
||||
@property() public filter!: string;
|
||||
|
||||
@@ -78,18 +78,18 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
.available=${addon.available}
|
||||
.icon=${addon.installed && addon.installed !== addon.version
|
||||
.icon=${addon.installed && addon.update_available
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? addon.update_available
|
||||
? "New version available"
|
||||
: "Add-on is installed"
|
||||
: addon.available
|
||||
? "Add-on is not installed"
|
||||
: "Add-on is not available on your system"}
|
||||
.iconClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? addon.update_available
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
@@ -104,7 +104,7 @@ class HassioAddonRepositoryEl extends LitElement {
|
||||
: undefined}
|
||||
.showTopbar=${addon.installed || !addon.available}
|
||||
.topbarClass=${addon.installed
|
||||
? addon.installed !== addon.version
|
||||
? addon.update_available
|
||||
? "update"
|
||||
: "installed"
|
||||
: !addon.available
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
PropertyValues,
|
||||
} from "lit-element";
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
import { atLeastVersion } from "../../../src/common/config/version";
|
||||
import "../../../src/common/search/search-input";
|
||||
import "../../../src/components/ha-button-menu";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
@@ -24,6 +25,7 @@ import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-tabs-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
import { showRegistriesDialog } from "../dialogs/registries/show-dialog-registries";
|
||||
import { showRepositoriesDialog } from "../dialogs/repositories/show-dialog-repositories";
|
||||
import { supervisorTabs } from "../hassio-tabs";
|
||||
import "./hassio-addon-repository";
|
||||
@@ -113,6 +115,12 @@ class HassioAddonStore extends LitElement {
|
||||
<mwc-list-item>
|
||||
Reload
|
||||
</mwc-list-item>
|
||||
${this.hass.userData?.showAdvanced &&
|
||||
atLeastVersion(this.hass.config.version, 0, 117)
|
||||
? html`<mwc-list-item>
|
||||
Registries
|
||||
</mwc-list-item>`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
${repos.length === 0
|
||||
? html`<hass-loading-screen no-toolbar></hass-loading-screen>`
|
||||
@@ -157,6 +165,9 @@ class HassioAddonStore extends LitElement {
|
||||
case 1:
|
||||
this.refreshData();
|
||||
break;
|
||||
case 2:
|
||||
this._manageRegistries();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,6 +184,10 @@ class HassioAddonStore extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _manageRegistries() {
|
||||
showRegistriesDialog(this);
|
||||
}
|
||||
|
||||
private async _loadData() {
|
||||
try {
|
||||
const addonsInfo = await fetchHassioAddonsInfo(this.hass);
|
||||
|
||||
@@ -39,13 +39,11 @@ class HassioAddonConfig extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) private _configHasChanged = false;
|
||||
|
||||
@property({ type: Boolean }) private _valid = true;
|
||||
|
||||
@query("ha-yaml-editor", true) private _editor!: HaYamlEditor;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const editor = this._editor;
|
||||
// If editor not rendered, don't show the error.
|
||||
const valid = editor ? editor.isValid : true;
|
||||
|
||||
return html`
|
||||
<h1>${this.addon.name}</h1>
|
||||
<ha-card header="Configuration">
|
||||
@@ -54,7 +52,7 @@ class HassioAddonConfig extends LitElement {
|
||||
@value-changed=${this._configChanged}
|
||||
></ha-yaml-editor>
|
||||
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
|
||||
${valid ? "" : html` <div class="errors">Invalid YAML</div> `}
|
||||
${this._valid ? "" : html` <div class="errors">Invalid YAML</div> `}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-progress-button class="warning" @click=${this._resetTapped}>
|
||||
@@ -62,7 +60,7 @@ class HassioAddonConfig extends LitElement {
|
||||
</ha-progress-button>
|
||||
<ha-progress-button
|
||||
@click=${this._saveTapped}
|
||||
.disabled=${!this._configHasChanged || !valid}
|
||||
.disabled=${!this._configHasChanged || !this._valid}
|
||||
>
|
||||
Save
|
||||
</ha-progress-button>
|
||||
@@ -78,9 +76,9 @@ class HassioAddonConfig extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _configChanged(): void {
|
||||
private _configChanged(ev): void {
|
||||
this._configHasChanged = true;
|
||||
this.requestUpdate();
|
||||
this._valid = ev.detail.isValid;
|
||||
}
|
||||
|
||||
private async _resetTapped(ev: CustomEvent): Promise<void> {
|
||||
|
||||
@@ -135,7 +135,7 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this._computeUpdateAvailable
|
||||
${this.addon.update_available
|
||||
? html`
|
||||
<ha-card header="Update available! 🎉">
|
||||
<div class="card-content">
|
||||
@@ -178,7 +178,7 @@ class HassioAddonInfo extends LitElement {
|
||||
${!this.addon.protected
|
||||
? html`
|
||||
<ha-card class="warning">
|
||||
<div class="card-header">Warning: Protection mode is disabled!</div>
|
||||
<h1 class="card-header">Warning: Protection mode is disabled!</h1>
|
||||
<div class="card-content">
|
||||
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
|
||||
</div>
|
||||
@@ -609,15 +609,6 @@ class HassioAddonInfo extends LitElement {
|
||||
return this.addon?.state === "started";
|
||||
}
|
||||
|
||||
private get _computeUpdateAvailable(): boolean | "" {
|
||||
return (
|
||||
this.addon &&
|
||||
!this.addon.detached &&
|
||||
this.addon.version &&
|
||||
this.addon.version !== this.addon.version_latest
|
||||
);
|
||||
}
|
||||
|
||||
private get _pathWebui(): string | null {
|
||||
return (
|
||||
this.addon.webui &&
|
||||
|
||||
@@ -50,7 +50,7 @@ class HassioCardContent extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<ha-svg-icon
|
||||
class=${this.iconClass}
|
||||
class=${this.iconClass!}
|
||||
.path=${this.icon}
|
||||
.title=${this.iconTitle}
|
||||
></ha-svg-icon>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import "../../../src/components/ha-file-upload";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiFolderUpload } from "@mdi/js";
|
||||
import "@polymer/iron-input/iron-input";
|
||||
@@ -12,13 +11,15 @@ import {
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/ha-circular-progress";
|
||||
import "../../../src/components/ha-file-upload";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
HassioSnapshot,
|
||||
uploadSnapshot,
|
||||
} from "../../../src/data/hassio/snapshot";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -65,7 +66,7 @@ export class HassioUploadSnapshot extends LitElement {
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Upload failed",
|
||||
text: err.toString(),
|
||||
text: extractApiErrorMessage(err),
|
||||
confirmText: "ok",
|
||||
});
|
||||
} finally {
|
||||
|
||||
@@ -52,22 +52,21 @@ class HassioAddons extends LitElement {
|
||||
.title=${addon.name}
|
||||
.description=${addon.description}
|
||||
available
|
||||
.showTopbar=${addon.installed !== addon.version}
|
||||
.showTopbar=${addon.update_available}
|
||||
topbarClass="update"
|
||||
.icon=${addon.installed !== addon.version
|
||||
.icon=${addon.update_available!
|
||||
? mdiArrowUpBoldCircle
|
||||
: mdiPuzzle}
|
||||
.iconTitle=${addon.state !== "started"
|
||||
? "Add-on is stopped"
|
||||
: addon.installed !== addon.version
|
||||
: addon.update_available!
|
||||
? "New version available"
|
||||
: "Add-on is running"}
|
||||
.iconClass=${addon.installed &&
|
||||
addon.installed !== addon.version
|
||||
.iconClass=${addon.update_available
|
||||
? addon.state === "started"
|
||||
? "update"
|
||||
: "update stopped"
|
||||
: addon.installed && addon.state === "started"
|
||||
: addon.state === "started"
|
||||
? "running"
|
||||
: "stopped"}
|
||||
.iconImage=${atLeastVersion(
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import memoizeOne from "memoize-one";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-svg-icon";
|
||||
@@ -35,29 +35,30 @@ import { hassioStyle } from "../resources/hassio-style";
|
||||
export class HassioUpdate extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
|
||||
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo;
|
||||
|
||||
@property() public supervisorInfo: HassioSupervisorInfo;
|
||||
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
@internalProperty() private _error?: string;
|
||||
private _pendingUpdates = memoizeOne(
|
||||
(
|
||||
core?: HassioHomeAssistantInfo,
|
||||
supervisor?: HassioSupervisorInfo,
|
||||
os?: HassioHassOSInfo
|
||||
): number => {
|
||||
return [core, supervisor, os].filter(
|
||||
(value) => !!value && value?.update_available
|
||||
).length;
|
||||
}
|
||||
);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const updatesAvailable: number = [
|
||||
const updatesAvailable = this._pendingUpdates(
|
||||
this.hassInfo,
|
||||
this.supervisorInfo,
|
||||
this.hassOsInfo,
|
||||
].filter((value) => {
|
||||
return (
|
||||
!!value &&
|
||||
(value.version_latest
|
||||
? value.version !== value.version_latest
|
||||
: value.version_latest
|
||||
? value.version !== value.version_latest
|
||||
: false)
|
||||
);
|
||||
}).length;
|
||||
this.hassOsInfo
|
||||
);
|
||||
|
||||
if (!updatesAvailable) {
|
||||
return html``;
|
||||
@@ -65,9 +66,6 @@ export class HassioUpdate extends LitElement {
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
${this._error
|
||||
? html` <div class="error">Error: ${this._error}</div> `
|
||||
: ""}
|
||||
<h1>
|
||||
${updatesAvailable > 1
|
||||
? "Updates Available 🎉"
|
||||
@@ -76,26 +74,24 @@ export class HassioUpdate extends LitElement {
|
||||
<div class="card-group">
|
||||
${this._renderUpdateCard(
|
||||
"Home Assistant Core",
|
||||
this.hassInfo.version,
|
||||
this.hassInfo.version_latest,
|
||||
this.hassInfo!,
|
||||
"hassio/homeassistant/update",
|
||||
`https://${
|
||||
this.hassInfo.version_latest.includes("b") ? "rc" : "www"
|
||||
}.home-assistant.io/latest-release-notes/`,
|
||||
mdiHomeAssistant
|
||||
this.hassInfo?.version_latest.includes("b") ? "rc" : "www"
|
||||
}.home-assistant.io/latest-release-notes/`
|
||||
)}
|
||||
${this._renderUpdateCard(
|
||||
"Supervisor",
|
||||
this.supervisorInfo.version,
|
||||
this.supervisorInfo.version_latest,
|
||||
this.supervisorInfo!,
|
||||
"hassio/supervisor/update",
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.version_latest}`
|
||||
`https://github.com//home-assistant/hassio/releases/tag/${
|
||||
this.supervisorInfo!.version_latest
|
||||
}`
|
||||
)}
|
||||
${this.hassOsInfo
|
||||
? this._renderUpdateCard(
|
||||
"Operating System",
|
||||
this.hassOsInfo.version,
|
||||
this.hassOsInfo.version_latest,
|
||||
this.hassOsInfo,
|
||||
"hassio/os/update",
|
||||
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
|
||||
)
|
||||
@@ -107,28 +103,22 @@ export class HassioUpdate extends LitElement {
|
||||
|
||||
private _renderUpdateCard(
|
||||
name: string,
|
||||
curVersion: string,
|
||||
lastVersion: string,
|
||||
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
|
||||
apiPath: string,
|
||||
releaseNotesUrl: string,
|
||||
icon?: string
|
||||
releaseNotesUrl: string
|
||||
): TemplateResult {
|
||||
if (!lastVersion || lastVersion === curVersion) {
|
||||
if (!object.update_available) {
|
||||
return html``;
|
||||
}
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
${icon
|
||||
? html`
|
||||
<div class="icon">
|
||||
<ha-svg-icon .path=${icon}></ha-svg-icon>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
<div class="update-heading">${name} ${lastVersion}</div>
|
||||
<div class="icon">
|
||||
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
|
||||
</div>
|
||||
<div class="update-heading">${name} ${object.version_latest}</div>
|
||||
<div class="warning">
|
||||
You are currently running version ${curVersion}
|
||||
You are currently running version ${object.version}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -138,7 +128,7 @@ export class HassioUpdate extends LitElement {
|
||||
<ha-progress-button
|
||||
.apiPath=${apiPath}
|
||||
.name=${name}
|
||||
.version=${lastVersion}
|
||||
.version=${object.version_latest}
|
||||
@click=${this._confirmUpdate}
|
||||
>
|
||||
Update
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDelete } from "@mdi/js";
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../../src/components/ha-circular-progress";
|
||||
import { createCloseHeading } from "../../../../src/components/ha-dialog";
|
||||
import "../../../../src/components/ha-svg-icon";
|
||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
|
||||
import {
|
||||
addHassioDockerRegistry,
|
||||
fetchHassioDockerRegistries,
|
||||
removeHassioDockerRegistry,
|
||||
} from "../../../../src/data/hassio/docker";
|
||||
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
|
||||
@customElement("dialog-hassio-registries")
|
||||
class HassioRegistriesDialog extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) private _registries?: {
|
||||
registry: string;
|
||||
username: string;
|
||||
}[];
|
||||
|
||||
@internalProperty() private _registry?: string;
|
||||
|
||||
@internalProperty() private _username?: string;
|
||||
|
||||
@internalProperty() private _password?: string;
|
||||
|
||||
@internalProperty() private _opened = false;
|
||||
|
||||
@internalProperty() private _addingRegistry = false;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-dialog
|
||||
.open=${this._opened}
|
||||
@closing=${this.closeDialog}
|
||||
scrimClickAction
|
||||
escapeKeyAction
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this._addingRegistry
|
||||
? "Add New Docker Registry"
|
||||
: "Manage Docker Registries"
|
||||
)}
|
||||
>
|
||||
<div class="form">
|
||||
${this._addingRegistry
|
||||
? html`
|
||||
<paper-input
|
||||
@value-changed=${this._inputChanged}
|
||||
class="flex-auto"
|
||||
name="registry"
|
||||
label="Registry"
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
<paper-input
|
||||
@value-changed=${this._inputChanged}
|
||||
class="flex-auto"
|
||||
name="username"
|
||||
label="Username"
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
<paper-input
|
||||
@value-changed=${this._inputChanged}
|
||||
class="flex-auto"
|
||||
name="password"
|
||||
label="Password"
|
||||
type="password"
|
||||
required
|
||||
auto-validate
|
||||
></paper-input>
|
||||
|
||||
<mwc-button
|
||||
?disabled=${Boolean(
|
||||
!this._registry || !this._username || !this._password
|
||||
)}
|
||||
@click=${this._addNewRegistry}
|
||||
>
|
||||
Add registry
|
||||
</mwc-button>
|
||||
`
|
||||
: html`${this._registries?.length
|
||||
? this._registries.map((entry) => {
|
||||
return html`
|
||||
<mwc-list-item class="option" hasMeta twoline>
|
||||
<span>${entry.registry}</span>
|
||||
<span slot="secondary"
|
||||
>Username: ${entry.username}</span
|
||||
>
|
||||
<mwc-icon-button
|
||||
.entry=${entry}
|
||||
title="Remove"
|
||||
slot="meta"
|
||||
@click=${this._removeRegistry}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</mwc-list-item>
|
||||
`;
|
||||
})
|
||||
: html`
|
||||
<mwc-list-item>
|
||||
<span>No registries configured</span>
|
||||
</mwc-list-item>
|
||||
`}
|
||||
<mwc-button @click=${this._addRegistry}>
|
||||
Add new registry
|
||||
</mwc-button> `}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _inputChanged(ev: Event) {
|
||||
const target = ev.currentTarget as PaperInputElement;
|
||||
this[`_${target.name}`] = target.value;
|
||||
}
|
||||
|
||||
public async showDialog(_dialogParams: any): Promise<void> {
|
||||
this._opened = true;
|
||||
await this._loadRegistries();
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._addingRegistry = false;
|
||||
this._opened = false;
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
this.updateComplete.then(() =>
|
||||
(this.shadowRoot?.querySelector(
|
||||
"[dialogInitialFocus]"
|
||||
) as HTMLElement)?.focus()
|
||||
);
|
||||
}
|
||||
|
||||
private async _loadRegistries(): Promise<void> {
|
||||
const registries = await fetchHassioDockerRegistries(this.hass);
|
||||
this._registries = Object.keys(registries!.registries).map((key) => ({
|
||||
registry: key,
|
||||
username: registries.registries[key].username,
|
||||
}));
|
||||
}
|
||||
|
||||
private _addRegistry(): void {
|
||||
this._addingRegistry = true;
|
||||
}
|
||||
|
||||
private async _addNewRegistry(): Promise<void> {
|
||||
const data = {};
|
||||
data[this._registry!] = {
|
||||
username: this._username,
|
||||
password: this._password,
|
||||
};
|
||||
|
||||
try {
|
||||
await addHassioDockerRegistry(this.hass, data);
|
||||
await this._loadRegistries();
|
||||
this._addingRegistry = false;
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to add registry",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async _removeRegistry(ev: Event): Promise<void> {
|
||||
const entry = (ev.currentTarget as any).entry;
|
||||
|
||||
try {
|
||||
await removeHassioDockerRegistry(this.hass, entry.registry);
|
||||
await this._loadRegistries();
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to remove registry",
|
||||
text: extractApiErrorMessage(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog.button-left {
|
||||
--justify-action-buttons: flex-start;
|
||||
}
|
||||
paper-icon-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
.form {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.option {
|
||||
border: 1px solid var(--divider-color);
|
||||
border-radius: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
mwc-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
mwc-icon-button {
|
||||
color: var(--error-color);
|
||||
margin: -10px;
|
||||
}
|
||||
mwc-list-item {
|
||||
cursor: default;
|
||||
}
|
||||
mwc-list-item span[slot="secondary"] {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-paper-dropdown-menu {
|
||||
display: block;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-hassio-registries": HassioRegistriesDialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { fireEvent } from "../../../../src/common/dom/fire_event";
|
||||
import "./dialog-hassio-registries";
|
||||
|
||||
export const showRegistriesDialog = (element: HTMLElement): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-hassio-registries",
|
||||
dialogImport: () =>
|
||||
import(
|
||||
/* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries"
|
||||
),
|
||||
dialogParams: {},
|
||||
});
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import "@material/mwc-button";
|
||||
import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
|
||||
import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-checkbox/paper-checkbox";
|
||||
import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import {
|
||||
css,
|
||||
|
||||
@@ -25,13 +25,13 @@ class HassioPanelRouter extends HassRouterPage {
|
||||
|
||||
@property({ type: Boolean }) public narrow!: boolean;
|
||||
|
||||
@property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo;
|
||||
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
@property({ attribute: false }) public hassioInfo!: HassioInfo;
|
||||
|
||||
@property({ attribute: false }) public hostInfo: HassioHostInfo;
|
||||
@property({ attribute: false }) public hostInfo?: HassioHostInfo;
|
||||
|
||||
@property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
|
||||
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
|
||||
|
||||
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
|
||||
|
||||
|
||||
@@ -66,15 +66,15 @@ class HassioRouter extends HassRouterPage {
|
||||
},
|
||||
};
|
||||
|
||||
@internalProperty() private _supervisorInfo: HassioSupervisorInfo;
|
||||
@internalProperty() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
@internalProperty() private _hostInfo: HassioHostInfo;
|
||||
@internalProperty() private _hostInfo?: HassioHostInfo;
|
||||
|
||||
@internalProperty() private _hassioInfo?: HassioInfo;
|
||||
|
||||
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
|
||||
|
||||
@internalProperty() private _hassInfo: HassioHomeAssistantInfo;
|
||||
@internalProperty() private _hassInfo?: HassioHomeAssistantInfo;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { createHassioSession } from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
createHassioSession,
|
||||
validateHassioSession,
|
||||
} from "../../../src/data/hassio/ingress";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
@@ -35,6 +38,17 @@ class HassioIngressView extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public narrow = false;
|
||||
|
||||
private _sessionKeepAlive?: number;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
this._sessionKeepAlive = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addon) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
@@ -83,10 +97,7 @@ class HassioIngressView extends LitElement {
|
||||
}
|
||||
|
||||
private async _fetchData(addonSlug: string) {
|
||||
const createSessionPromise = createHassioSession(this.hass).then(
|
||||
() => true,
|
||||
() => false
|
||||
);
|
||||
const createSessionPromise = createHassioSession(this.hass);
|
||||
|
||||
let addon;
|
||||
|
||||
@@ -119,7 +130,11 @@ class HassioIngressView extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await createSessionPromise)) {
|
||||
let session;
|
||||
|
||||
try {
|
||||
session = await createSessionPromise;
|
||||
} catch (err) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to create an Ingress session",
|
||||
title: addon.name,
|
||||
@@ -128,6 +143,17 @@ class HassioIngressView extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
}
|
||||
this._sessionKeepAlive = window.setInterval(async () => {
|
||||
try {
|
||||
await validateHassioSession(this.hass, session);
|
||||
} catch (err) {
|
||||
session = await createHassioSession(this.hass);
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
this._addon = addon;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,8 +108,8 @@ class HassioHostInfo extends LitElement {
|
||||
<span slot="description">
|
||||
${this.hostInfo.operating_system}
|
||||
</span>
|
||||
${this.hostInfo.version !== this.hostInfo.version_latest &&
|
||||
this.hostInfo.features.includes("hassos")
|
||||
${this.hostInfo.features.includes("hassos") &&
|
||||
this.hassOsInfo.update_available
|
||||
? html`
|
||||
<ha-progress-button
|
||||
title="Update the host OS"
|
||||
|
||||
@@ -7,18 +7,21 @@ import {
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../../src/common/dom/fire_event";
|
||||
import "../../../src/components/buttons/ha-progress-button";
|
||||
import "../../../src/components/ha-card";
|
||||
import "../../../src/components/ha-settings-row";
|
||||
import "../../../src/components/ha-switch";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
|
||||
import { fetchHassioResolution } from "../../../src/data/hassio/resolution";
|
||||
import {
|
||||
fetchHassioSupervisorInfo,
|
||||
HassioSupervisorInfo as HassioSupervisorInfoType,
|
||||
reloadSupervisor,
|
||||
setSupervisorOption,
|
||||
SupervisorOptions,
|
||||
updateSupervisor,
|
||||
fetchHassioSupervisorInfo,
|
||||
} from "../../../src/data/hassio/supervisor";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -26,14 +29,42 @@ import {
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { documentationUrl } from "../../../src/util/documentation-url";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
|
||||
const ISSUES = {
|
||||
container: {
|
||||
title: "Containers known to cause issues",
|
||||
url: "/more-info/unsupported/container",
|
||||
},
|
||||
dbus: { title: "DBUS", url: "/more-info/unsupported/dbus" },
|
||||
docker_configuration: {
|
||||
title: "Docker Configuration",
|
||||
url: "/more-info/unsupported/docker_configuration",
|
||||
},
|
||||
docker_version: {
|
||||
title: "Docker Version",
|
||||
url: "/more-info/unsupported/docker_version",
|
||||
},
|
||||
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
|
||||
network_manager: {
|
||||
title: "Network Manager",
|
||||
url: "/more-info/unsupported/network_manager",
|
||||
},
|
||||
os: { title: "Operating System", url: "/more-info/unsupported/os" },
|
||||
privileged: {
|
||||
title: "Supervisor is not privileged",
|
||||
url: "/more-info/unsupported/privileged",
|
||||
},
|
||||
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
|
||||
};
|
||||
|
||||
@customElement("hassio-supervisor-info")
|
||||
class HassioSupervisorInfo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public supervisorInfo!: HassioSupervisorInfoType;
|
||||
@property({ attribute: false })
|
||||
public supervisorInfo!: HassioSupervisorInfoType;
|
||||
|
||||
@property() public hostInfo!: HassioHostInfoType;
|
||||
|
||||
@@ -56,7 +87,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
<span slot="description">
|
||||
${this.supervisorInfo.version_latest}
|
||||
</span>
|
||||
${this.supervisorInfo.version !== this.supervisorInfo.version_latest
|
||||
${this.supervisorInfo.update_available
|
||||
? html`
|
||||
<ha-progress-button
|
||||
title="Update the supervisor"
|
||||
@@ -118,18 +149,13 @@ class HassioSupervisorInfo extends LitElement {
|
||||
</ha-settings-row>`
|
||||
: html`<div class="error">
|
||||
You are running an unsupported installation.
|
||||
<a
|
||||
href="https://github.com/home-assistant/architecture/blob/master/adr/${this.hostInfo.features.includes(
|
||||
"hassos"
|
||||
)
|
||||
? "0015-home-assistant-os.md"
|
||||
: "0014-home-assistant-supervised.md"}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
<button
|
||||
class="link"
|
||||
title="Learn more about how you can make your system compliant"
|
||||
@click=${this._unsupportedDialog}
|
||||
>
|
||||
Learn More
|
||||
</a>
|
||||
Learn more
|
||||
</button>
|
||||
</div>`}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
@@ -181,7 +207,7 @@ class HassioSupervisorInfo extends LitElement {
|
||||
};
|
||||
await setSupervisorOption(this.hass, data);
|
||||
await reloadSupervisor(this.hass);
|
||||
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
fireEvent(this, "hass-api-called", { success: true, response: null });
|
||||
} catch (err) {
|
||||
showAlertDialog(this, {
|
||||
title: "Failed to set supervisor option",
|
||||
@@ -249,6 +275,32 @@ class HassioSupervisorInfo extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private async _unsupportedDialog(): Promise<void> {
|
||||
const resolution = await fetchHassioResolution(this.hass);
|
||||
await showAlertDialog(this, {
|
||||
title: "You are running an unsupported installation",
|
||||
text: html`Below is a list of issues found with your installation, click
|
||||
on the links to learn how you can resolve the issues. <br /><br />
|
||||
<ul>
|
||||
${resolution.unsupported.map(
|
||||
(issue) => html`
|
||||
<li>
|
||||
${ISSUES[issue]
|
||||
? html`<a
|
||||
href="${documentationUrl(this.hass, ISSUES[issue].url)}"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
${ISSUES[issue].title}
|
||||
</a>`
|
||||
: issue}
|
||||
</li>
|
||||
`
|
||||
)}
|
||||
</ul>`,
|
||||
});
|
||||
}
|
||||
|
||||
private async _toggleDiagnostics(): Promise<void> {
|
||||
try {
|
||||
const data: SupervisorOptions = {
|
||||
|
||||
@@ -21,11 +21,11 @@ import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common";
|
||||
import { HassioHostInfo } from "../../../src/data/hassio/host";
|
||||
import { haStyle } from "../../../src/resources/styles";
|
||||
import { HomeAssistant } from "../../../src/types";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
import {
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../src/util/calculate";
|
||||
import { bytesToString } from "../../../src/util/bytes-to-string";
|
||||
import { hassioStyle } from "../resources/hassio-style";
|
||||
|
||||
@customElement("hassio-system-metrics")
|
||||
@@ -65,9 +65,7 @@ class HassioSystemMetrics extends LitElement {
|
||||
{
|
||||
description: "Used Space",
|
||||
value: this._getUsedSpace(this.hostInfo),
|
||||
tooltip: `${
|
||||
this.hostInfo.disk_used
|
||||
} GB/${this.hostInfo.disk_total} GB`,
|
||||
tooltip: `${this.hostInfo.disk_used} GB/${this.hostInfo.disk_total} GB`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
+9
-8
@@ -22,7 +22,8 @@
|
||||
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@formatjs/intl-pluralrules": "^1.5.8",
|
||||
"@formatjs/intl-getcanonicallocales": "^1.4.6",
|
||||
"@formatjs/intl-pluralrules": "^3.4.10",
|
||||
"@fullcalendar/common": "5.1.0",
|
||||
"@fullcalendar/core": "5.1.0",
|
||||
"@fullcalendar/daygrid": "5.1.0",
|
||||
@@ -77,7 +78,7 @@
|
||||
"@polymer/paper-toast": "^3.0.1",
|
||||
"@polymer/paper-tooltip": "^3.0.1",
|
||||
"@polymer/polymer": "3.1.0",
|
||||
"@thomasloven/round-slider": "0.5.0",
|
||||
"@thomasloven/round-slider": "0.5.2",
|
||||
"@types/chromecast-caf-sender": "^1.0.3",
|
||||
"@types/sortablejs": "^1.10.6",
|
||||
"@vaadin/vaadin-combo-box": "^5.0.10",
|
||||
@@ -109,7 +110,7 @@
|
||||
"marked": "^1.1.1",
|
||||
"mdn-polyfills": "^5.16.0",
|
||||
"memoize-one": "^5.0.2",
|
||||
"node-vibrant": "^3.1.5",
|
||||
"node-vibrant": "^3.1.6",
|
||||
"proxy-polyfill": "^0.3.1",
|
||||
"punycode": "^2.1.1",
|
||||
"qrcode": "^1.4.4",
|
||||
@@ -118,6 +119,7 @@
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"sortablejs": "^1.10.2",
|
||||
"superstruct": "^0.10.12",
|
||||
"tinykeys": "^1.1.1",
|
||||
"unfetch": "^4.1.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue2-daterange-picker": "^0.5.1",
|
||||
@@ -175,7 +177,6 @@
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-foreach": "^0.1.0",
|
||||
"gulp-json-transform": "^0.4.6",
|
||||
"gulp-jsonminify": "^1.1.0",
|
||||
"gulp-merge-json": "^1.3.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-zopfli-green": "^3.0.1",
|
||||
@@ -202,15 +203,15 @@
|
||||
"sinon": "^7.3.1",
|
||||
"source-map-url": "^0.4.0",
|
||||
"systemjs": "^6.3.2",
|
||||
"terser-webpack-plugin": "^3.0.6",
|
||||
"terser-webpack-plugin": "^5.0.0",
|
||||
"ts-lit-plugin": "^1.2.1",
|
||||
"ts-mocha": "^7.0.0",
|
||||
"typescript": "^4.0.3",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vinyl-source-stream": "^2.0.0",
|
||||
"webpack": "5.0.0-rc.3",
|
||||
"webpack-cli": "4.0.0-rc.0",
|
||||
"webpack-dev-server": "^3.10.3",
|
||||
"webpack": "5.1.3",
|
||||
"webpack-cli": "4.1.0",
|
||||
"webpack-dev-server": "^3.11.0",
|
||||
"webpack-manifest-plugin": "3.0.0-rc.0",
|
||||
"workbox-build": "^5.1.3"
|
||||
},
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
"src/panels/iframe/ha-panel-iframe.js",
|
||||
"src/panels/logbook/ha-panel-logbook.js",
|
||||
"src/panels/map/ha-panel-map.js",
|
||||
"src/panels/shopping-list/ha-panel-shopping-list.js",
|
||||
"src/panels/mailbox/ha-panel-mailbox.js",
|
||||
"hassio/src/entrypoint.js"
|
||||
],
|
||||
|
||||
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20201001.0",
|
||||
version="20201021.1",
|
||||
description="The Home Assistant frontend",
|
||||
url="https://github.com/home-assistant/home-assistant-polymer",
|
||||
author="The Home Assistant Authors",
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
const expand_hex = (hex: string): string => {
|
||||
let result = "";
|
||||
for (const val of hex) {
|
||||
result += val + val;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
import { expandHex } from "./hex";
|
||||
|
||||
const rgb_hex = (component: number): string => {
|
||||
const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16);
|
||||
@@ -14,10 +8,7 @@ const rgb_hex = (component: number): string => {
|
||||
// Conversion between HEX and RGB
|
||||
|
||||
export const hex2rgb = (hex: string): [number, number, number] => {
|
||||
hex = hex.replace("#", "");
|
||||
if (hex.length === 3 || hex.length === 4) {
|
||||
hex = expand_hex(hex);
|
||||
}
|
||||
hex = expandHex(hex);
|
||||
|
||||
return [
|
||||
parseInt(hex.substring(0, 2), 16),
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
export const expandHex = (hex: string): string => {
|
||||
hex = hex.replace("#", "");
|
||||
if (hex.length === 6) return hex;
|
||||
let result = "";
|
||||
for (const val of hex) {
|
||||
result += val + val;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity.
|
||||
export const hexBlend = (c1: string, c2: string, blend = 50): string => {
|
||||
let color = "";
|
||||
c1 = expandHex(c1);
|
||||
c2 = expandHex(c2);
|
||||
for (let i = 0; i <= 5; i += 2) {
|
||||
const h1 = parseInt(c1.substr(i, 2), 16);
|
||||
const h2 = parseInt(c2.substr(i, 2), 16);
|
||||
let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16);
|
||||
while (hex.length < 2) hex = "0" + hex;
|
||||
color += hex;
|
||||
}
|
||||
return `#${color}`;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
rgb2hex,
|
||||
rgb2lab,
|
||||
} from "../color/convert-color";
|
||||
import { hexBlend } from "../color/hex";
|
||||
import { labBrighten, labDarken } from "../color/lab";
|
||||
import { rgbContrast } from "../color/rgb";
|
||||
|
||||
@@ -37,6 +38,13 @@ export const applyThemesOnElement = (
|
||||
if (themeOptions.dark) {
|
||||
cacheKey = `${cacheKey}__dark`;
|
||||
themeRules = darkStyles;
|
||||
if (themeOptions.primaryColor) {
|
||||
themeRules["app-header-background-color"] = hexBlend(
|
||||
themeOptions.primaryColor,
|
||||
"#121212",
|
||||
8
|
||||
);
|
||||
}
|
||||
}
|
||||
if (themeOptions.primaryColor) {
|
||||
cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`;
|
||||
|
||||
@@ -10,10 +10,7 @@ export const dynamicElement = directive(
|
||||
|
||||
let element = part.value as HTMLElement | undefined;
|
||||
|
||||
if (
|
||||
element !== undefined &&
|
||||
tag.toUpperCase() === (element as HTMLElement).tagName
|
||||
) {
|
||||
if (tag === element?.localName) {
|
||||
if (properties) {
|
||||
Object.entries(properties).forEach(([key, value]) => {
|
||||
element![key] = value;
|
||||
|
||||
@@ -23,7 +23,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
|
||||
case "problem":
|
||||
case "safety":
|
||||
case "smoke":
|
||||
return is_off ? "hass:shield-check" : "hass:alert";
|
||||
return is_off ? "hass:check-circle" : "hass:alert-circle";
|
||||
case "heat":
|
||||
return is_off ? "hass:thermometer" : "hass:fire";
|
||||
case "light":
|
||||
|
||||
@@ -5,6 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time";
|
||||
import { formatTime } from "../datetime/format_time";
|
||||
import { LocalizeFunc } from "../translations/localize";
|
||||
import { computeStateDomain } from "./compute_state_domain";
|
||||
import { numberFormat } from "../string/number-format";
|
||||
|
||||
export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
@@ -19,7 +20,9 @@ export const computeStateDisplay = (
|
||||
}
|
||||
|
||||
if (stateObj.attributes.unit_of_measurement) {
|
||||
return `${compareState} ${stateObj.attributes.unit_of_measurement}`;
|
||||
return `${numberFormat(compareState, language)} ${
|
||||
stateObj.attributes.unit_of_measurement
|
||||
}`;
|
||||
}
|
||||
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
@@ -43,6 +43,7 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
|
||||
}
|
||||
case "blind":
|
||||
case "curtain":
|
||||
case "shade":
|
||||
switch (state) {
|
||||
case "opening":
|
||||
return "hass:arrow-up-box";
|
||||
|
||||
@@ -51,18 +51,14 @@ class SearchInput extends LitElement {
|
||||
@value-changed=${this._filterInputChanged}
|
||||
.noLabelFloat=${this.noLabelFloat}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${mdiMagnify}
|
||||
slot="prefix"
|
||||
class="prefix"
|
||||
></ha-svg-icon>
|
||||
<slot name="prefix" slot="prefix">
|
||||
<ha-svg-icon class="prefix" .path=${mdiMagnify}></ha-svg-icon>
|
||||
</slot>
|
||||
${this.filter &&
|
||||
html`
|
||||
<mwc-icon-button
|
||||
slot="suffix"
|
||||
class="suffix"
|
||||
@click=${this._clearSearch}
|
||||
alt="Clear"
|
||||
title="Clear"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
|
||||
|
||||
/**
|
||||
* An inlined enum containing useful character codes (to be used with String.charCodeAt).
|
||||
* Please leave the const keyword such that it gets inlined when compiled to JavaScript!
|
||||
*/
|
||||
export enum CharCode {
|
||||
Null = 0,
|
||||
/**
|
||||
* The `\b` character.
|
||||
*/
|
||||
Backspace = 8,
|
||||
/**
|
||||
* The `\t` character.
|
||||
*/
|
||||
Tab = 9,
|
||||
/**
|
||||
* The `\n` character.
|
||||
*/
|
||||
LineFeed = 10,
|
||||
/**
|
||||
* The `\r` character.
|
||||
*/
|
||||
CarriageReturn = 13,
|
||||
Space = 32,
|
||||
/**
|
||||
* The `!` character.
|
||||
*/
|
||||
ExclamationMark = 33,
|
||||
/**
|
||||
* The `"` character.
|
||||
*/
|
||||
DoubleQuote = 34,
|
||||
/**
|
||||
* The `#` character.
|
||||
*/
|
||||
Hash = 35,
|
||||
/**
|
||||
* The `$` character.
|
||||
*/
|
||||
DollarSign = 36,
|
||||
/**
|
||||
* The `%` character.
|
||||
*/
|
||||
PercentSign = 37,
|
||||
/**
|
||||
* The `&` character.
|
||||
*/
|
||||
Ampersand = 38,
|
||||
/**
|
||||
* The `'` character.
|
||||
*/
|
||||
SingleQuote = 39,
|
||||
/**
|
||||
* The `(` character.
|
||||
*/
|
||||
OpenParen = 40,
|
||||
/**
|
||||
* The `)` character.
|
||||
*/
|
||||
CloseParen = 41,
|
||||
/**
|
||||
* The `*` character.
|
||||
*/
|
||||
Asterisk = 42,
|
||||
/**
|
||||
* The `+` character.
|
||||
*/
|
||||
Plus = 43,
|
||||
/**
|
||||
* The `,` character.
|
||||
*/
|
||||
Comma = 44,
|
||||
/**
|
||||
* The `-` character.
|
||||
*/
|
||||
Dash = 45,
|
||||
/**
|
||||
* The `.` character.
|
||||
*/
|
||||
Period = 46,
|
||||
/**
|
||||
* The `/` character.
|
||||
*/
|
||||
Slash = 47,
|
||||
|
||||
Digit0 = 48,
|
||||
Digit1 = 49,
|
||||
Digit2 = 50,
|
||||
Digit3 = 51,
|
||||
Digit4 = 52,
|
||||
Digit5 = 53,
|
||||
Digit6 = 54,
|
||||
Digit7 = 55,
|
||||
Digit8 = 56,
|
||||
Digit9 = 57,
|
||||
|
||||
/**
|
||||
* The `:` character.
|
||||
*/
|
||||
Colon = 58,
|
||||
/**
|
||||
* The `;` character.
|
||||
*/
|
||||
Semicolon = 59,
|
||||
/**
|
||||
* The `<` character.
|
||||
*/
|
||||
LessThan = 60,
|
||||
/**
|
||||
* The `=` character.
|
||||
*/
|
||||
Equals = 61,
|
||||
/**
|
||||
* The `>` character.
|
||||
*/
|
||||
GreaterThan = 62,
|
||||
/**
|
||||
* The `?` character.
|
||||
*/
|
||||
QuestionMark = 63,
|
||||
/**
|
||||
* The `@` character.
|
||||
*/
|
||||
AtSign = 64,
|
||||
|
||||
A = 65,
|
||||
B = 66,
|
||||
C = 67,
|
||||
D = 68,
|
||||
E = 69,
|
||||
F = 70,
|
||||
G = 71,
|
||||
H = 72,
|
||||
I = 73,
|
||||
J = 74,
|
||||
K = 75,
|
||||
L = 76,
|
||||
M = 77,
|
||||
N = 78,
|
||||
O = 79,
|
||||
P = 80,
|
||||
Q = 81,
|
||||
R = 82,
|
||||
S = 83,
|
||||
T = 84,
|
||||
U = 85,
|
||||
V = 86,
|
||||
W = 87,
|
||||
X = 88,
|
||||
Y = 89,
|
||||
Z = 90,
|
||||
|
||||
/**
|
||||
* The `[` character.
|
||||
*/
|
||||
OpenSquareBracket = 91,
|
||||
/**
|
||||
* The `\` character.
|
||||
*/
|
||||
Backslash = 92,
|
||||
/**
|
||||
* The `]` character.
|
||||
*/
|
||||
CloseSquareBracket = 93,
|
||||
/**
|
||||
* The `^` character.
|
||||
*/
|
||||
Caret = 94,
|
||||
/**
|
||||
* The `_` character.
|
||||
*/
|
||||
Underline = 95,
|
||||
/**
|
||||
* The ``(`)`` character.
|
||||
*/
|
||||
BackTick = 96,
|
||||
|
||||
a = 97,
|
||||
b = 98,
|
||||
c = 99,
|
||||
d = 100,
|
||||
e = 101,
|
||||
f = 102,
|
||||
g = 103,
|
||||
h = 104,
|
||||
i = 105,
|
||||
j = 106,
|
||||
k = 107,
|
||||
l = 108,
|
||||
m = 109,
|
||||
n = 110,
|
||||
o = 111,
|
||||
p = 112,
|
||||
q = 113,
|
||||
r = 114,
|
||||
s = 115,
|
||||
t = 116,
|
||||
u = 117,
|
||||
v = 118,
|
||||
w = 119,
|
||||
x = 120,
|
||||
y = 121,
|
||||
z = 122,
|
||||
|
||||
/**
|
||||
* The `{` character.
|
||||
*/
|
||||
OpenCurlyBrace = 123,
|
||||
/**
|
||||
* The `|` character.
|
||||
*/
|
||||
Pipe = 124,
|
||||
/**
|
||||
* The `}` character.
|
||||
*/
|
||||
CloseCurlyBrace = 125,
|
||||
/**
|
||||
* The `~` character.
|
||||
*/
|
||||
Tilde = 126,
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
/* eslint-disable no-console */
|
||||
// MIT License
|
||||
|
||||
// Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { CharCode } from "./char-code";
|
||||
|
||||
const _debug = false;
|
||||
|
||||
export interface Match {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const _maxLen = 128;
|
||||
|
||||
function initTable() {
|
||||
const table: number[][] = [];
|
||||
const row: number[] = [0];
|
||||
for (let i = 1; i <= _maxLen; i++) {
|
||||
row.push(-i);
|
||||
}
|
||||
for (let i = 0; i <= _maxLen; i++) {
|
||||
const thisRow = row.slice(0);
|
||||
thisRow[0] = -i;
|
||||
table.push(thisRow);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
function isSeparatorAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.charCodeAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Underline:
|
||||
case CharCode.Dash:
|
||||
case CharCode.Period:
|
||||
case CharCode.Space:
|
||||
case CharCode.Slash:
|
||||
case CharCode.Backslash:
|
||||
case CharCode.SingleQuote:
|
||||
case CharCode.DoubleQuote:
|
||||
case CharCode.Colon:
|
||||
case CharCode.DollarSign:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isWhitespaceAtPos(value: string, index: number): boolean {
|
||||
if (index < 0 || index >= value.length) {
|
||||
return false;
|
||||
}
|
||||
const code = value.charCodeAt(index);
|
||||
switch (code) {
|
||||
case CharCode.Space:
|
||||
case CharCode.Tab:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
|
||||
return word[pos] !== wordLow[pos];
|
||||
}
|
||||
|
||||
function isPatternInWord(
|
||||
patternLow: string,
|
||||
patternPos: number,
|
||||
patternLen: number,
|
||||
wordLow: string,
|
||||
wordPos: number,
|
||||
wordLen: number
|
||||
): boolean {
|
||||
while (patternPos < patternLen && wordPos < wordLen) {
|
||||
if (patternLow[patternPos] === wordLow[wordPos]) {
|
||||
patternPos += 1;
|
||||
}
|
||||
wordPos += 1;
|
||||
}
|
||||
return patternPos === patternLen; // pattern must be exhausted
|
||||
}
|
||||
|
||||
enum Arrow {
|
||||
Top = 0b1,
|
||||
Diag = 0b10,
|
||||
Left = 0b100,
|
||||
}
|
||||
|
||||
/**
|
||||
* A tuple of three values.
|
||||
* 0. the score
|
||||
* 1. the matches encoded as bitmask (2^53)
|
||||
* 2. the offset at which matching started
|
||||
*/
|
||||
export type FuzzyScore = [number, number, number];
|
||||
|
||||
interface FilterGlobals {
|
||||
_matchesCount: number;
|
||||
_topMatch2: number;
|
||||
_topScore: number;
|
||||
_wordStart: number;
|
||||
_firstMatchCanBeWeak: boolean;
|
||||
_table: number[][];
|
||||
_scores: number[][];
|
||||
_arrows: Arrow[][];
|
||||
}
|
||||
|
||||
function initGlobals(): FilterGlobals {
|
||||
return {
|
||||
_matchesCount: 0,
|
||||
_topMatch2: 0,
|
||||
_topScore: 0,
|
||||
_wordStart: 0,
|
||||
_firstMatchCanBeWeak: false,
|
||||
_table: initTable(),
|
||||
_scores: initTable(),
|
||||
_arrows: <Arrow[][]>initTable(),
|
||||
};
|
||||
}
|
||||
|
||||
export function fuzzyScore(
|
||||
pattern: string,
|
||||
patternLow: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordStart: number,
|
||||
firstMatchCanBeWeak: boolean
|
||||
): FuzzyScore | undefined {
|
||||
const globals = initGlobals();
|
||||
const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
|
||||
const wordLen = word.length > _maxLen ? _maxLen : word.length;
|
||||
|
||||
if (
|
||||
patternStart >= patternLen ||
|
||||
wordStart >= wordLen ||
|
||||
patternLen - patternStart > wordLen - wordStart
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Run a simple check if the characters of pattern occur
|
||||
// (in order) at all in word. If that isn't the case we
|
||||
// stop because no match will be possible
|
||||
if (
|
||||
!isPatternInWord(
|
||||
patternLow,
|
||||
patternStart,
|
||||
patternLen,
|
||||
wordLow,
|
||||
wordStart,
|
||||
wordLen
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let row = 1;
|
||||
let column = 1;
|
||||
let patternPos = patternStart;
|
||||
let wordPos = wordStart;
|
||||
|
||||
let hasStrongFirstMatch = false;
|
||||
|
||||
// There will be a match, fill in tables
|
||||
for (
|
||||
row = 1, patternPos = patternStart;
|
||||
patternPos < patternLen;
|
||||
row++, patternPos++
|
||||
) {
|
||||
for (
|
||||
column = 1, wordPos = wordStart;
|
||||
wordPos < wordLen;
|
||||
column++, wordPos++
|
||||
) {
|
||||
const score = _doScore(
|
||||
pattern,
|
||||
patternLow,
|
||||
patternPos,
|
||||
patternStart,
|
||||
word,
|
||||
wordLow,
|
||||
wordPos
|
||||
);
|
||||
|
||||
if (patternPos === patternStart && score > 1) {
|
||||
hasStrongFirstMatch = true;
|
||||
}
|
||||
|
||||
globals._scores[row][column] = score;
|
||||
|
||||
const diag =
|
||||
globals._table[row - 1][column - 1] + (score > 1 ? 1 : score);
|
||||
const top = globals._table[row - 1][column] + -1;
|
||||
const left = globals._table[row][column - 1] + -1;
|
||||
|
||||
if (left >= top) {
|
||||
// left or diag
|
||||
if (left > diag) {
|
||||
globals._table[row][column] = left;
|
||||
globals._arrows[row][column] = Arrow.Left;
|
||||
} else if (left === diag) {
|
||||
globals._table[row][column] = left;
|
||||
globals._arrows[row][column] = Arrow.Left || Arrow.Diag;
|
||||
} else {
|
||||
globals._table[row][column] = diag;
|
||||
globals._arrows[row][column] = Arrow.Diag;
|
||||
}
|
||||
} else if (top > diag) {
|
||||
globals._table[row][column] = top;
|
||||
globals._arrows[row][column] = Arrow.Top;
|
||||
} else if (top === diag) {
|
||||
globals._table[row][column] = top;
|
||||
globals._arrows[row][column] = Arrow.Top || Arrow.Diag;
|
||||
} else {
|
||||
globals._table[row][column] = diag;
|
||||
globals._arrows[row][column] = Arrow.Diag;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_debug) {
|
||||
printTables(pattern, patternStart, word, wordStart, globals);
|
||||
}
|
||||
|
||||
if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
globals._matchesCount = 0;
|
||||
globals._topScore = -100;
|
||||
globals._wordStart = wordStart;
|
||||
globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
|
||||
|
||||
_findAllMatches2(
|
||||
row - 1,
|
||||
column - 1,
|
||||
patternLen === wordLen ? 1 : 0,
|
||||
0,
|
||||
false,
|
||||
globals
|
||||
);
|
||||
if (globals._matchesCount === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [globals._topScore, globals._topMatch2, wordStart];
|
||||
}
|
||||
|
||||
function _doScore(
|
||||
pattern: string,
|
||||
patternLow: string,
|
||||
patternPos: number,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordLow: string,
|
||||
wordPos: number
|
||||
) {
|
||||
if (patternLow[patternPos] !== wordLow[wordPos]) {
|
||||
return -1;
|
||||
}
|
||||
if (wordPos === patternPos - patternStart) {
|
||||
// common prefix: `foobar <-> foobaz`
|
||||
// ^^^^^
|
||||
if (pattern[patternPos] === word[wordPos]) {
|
||||
return 7;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (
|
||||
isUpperCaseAtPos(wordPos, word, wordLow) &&
|
||||
(wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
|
||||
) {
|
||||
// hitting upper-case: `foo <-> forOthers`
|
||||
// ^^ ^
|
||||
if (pattern[patternPos] === word[wordPos]) {
|
||||
return 7;
|
||||
}
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (
|
||||
isSeparatorAtPos(wordLow, wordPos) &&
|
||||
(wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
|
||||
) {
|
||||
// hitting a separator: `. <-> foo.bar`
|
||||
// ^
|
||||
return 5;
|
||||
}
|
||||
|
||||
if (
|
||||
isSeparatorAtPos(wordLow, wordPos - 1) ||
|
||||
isWhitespaceAtPos(wordLow, wordPos - 1)
|
||||
) {
|
||||
// post separator: `foo <-> bar_foo`
|
||||
// ^^^
|
||||
return 5;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function printTable(
|
||||
table: number[][],
|
||||
pattern: string,
|
||||
patternLen: number,
|
||||
word: string,
|
||||
wordLen: number
|
||||
): string {
|
||||
function pad(s: string, n: number, _pad = " ") {
|
||||
while (s.length < n) {
|
||||
s = _pad + s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
let ret = ` | |${word
|
||||
.split("")
|
||||
.map((c) => pad(c, 3))
|
||||
.join("|")}\n`;
|
||||
|
||||
for (let i = 0; i <= patternLen; i++) {
|
||||
if (i === 0) {
|
||||
ret += " |";
|
||||
} else {
|
||||
ret += `${pattern[i - 1]}|`;
|
||||
}
|
||||
ret +=
|
||||
table[i]
|
||||
.slice(0, wordLen + 1)
|
||||
.map((n) => pad(n.toString(), 3))
|
||||
.join("|") + "\n";
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
function printTables(
|
||||
pattern: string,
|
||||
patternStart: number,
|
||||
word: string,
|
||||
wordStart: number,
|
||||
globals: FilterGlobals
|
||||
): void {
|
||||
pattern = pattern.substr(patternStart);
|
||||
word = word.substr(wordStart);
|
||||
console.log(
|
||||
printTable(globals._table, pattern, pattern.length, word, word.length)
|
||||
);
|
||||
console.log(
|
||||
printTable(globals._arrows, pattern, pattern.length, word, word.length)
|
||||
);
|
||||
console.log(
|
||||
printTable(globals._scores, pattern, pattern.length, word, word.length)
|
||||
);
|
||||
}
|
||||
|
||||
function _findAllMatches2(
|
||||
row: number,
|
||||
column: number,
|
||||
total: number,
|
||||
matches: number,
|
||||
lastMatched: boolean,
|
||||
globals: FilterGlobals
|
||||
): void {
|
||||
if (globals._matchesCount >= 10 || total < -25) {
|
||||
// stop when having already 10 results, or
|
||||
// when a potential alignment as already 5 gaps
|
||||
return;
|
||||
}
|
||||
|
||||
let simpleMatchCount = 0;
|
||||
|
||||
while (row > 0 && column > 0) {
|
||||
const score = globals._scores[row][column];
|
||||
const arrow = globals._arrows[row][column];
|
||||
|
||||
if (arrow === Arrow.Left) {
|
||||
// left -> no match, skip a word character
|
||||
column -= 1;
|
||||
if (lastMatched) {
|
||||
total -= 5; // new gap penalty
|
||||
} else if (matches !== 0) {
|
||||
total -= 1; // gap penalty after first match
|
||||
}
|
||||
lastMatched = false;
|
||||
simpleMatchCount = 0;
|
||||
} else if (arrow && Arrow.Diag) {
|
||||
if (arrow && Arrow.Left) {
|
||||
// left
|
||||
_findAllMatches2(
|
||||
row,
|
||||
column - 1,
|
||||
matches !== 0 ? total - 1 : total, // gap penalty after first match
|
||||
matches,
|
||||
lastMatched,
|
||||
globals
|
||||
);
|
||||
}
|
||||
|
||||
// diag
|
||||
total += score;
|
||||
row -= 1;
|
||||
column -= 1;
|
||||
lastMatched = true;
|
||||
|
||||
// match -> set a 1 at the word pos
|
||||
matches += 2 ** (column + globals._wordStart);
|
||||
|
||||
// count simple matches and boost a row of
|
||||
// simple matches when they yield in a
|
||||
// strong match.
|
||||
if (score === 1) {
|
||||
simpleMatchCount += 1;
|
||||
|
||||
if (row === 0 && !globals._firstMatchCanBeWeak) {
|
||||
// when the first match is a weak
|
||||
// match we discard it
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// boost
|
||||
total += 1 + simpleMatchCount * (score - 1);
|
||||
simpleMatchCount = 0;
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
total -= column >= 3 ? 9 : column * 3; // late start penalty
|
||||
|
||||
// dynamically keep track of the current top score
|
||||
// and insert the current best score at head, the rest at tail
|
||||
globals._matchesCount += 1;
|
||||
if (total > globals._topScore) {
|
||||
globals._topScore = total;
|
||||
globals._topMatch2 = matches;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion
|
||||
@@ -0,0 +1,66 @@
|
||||
import { fuzzyScore } from "./filter";
|
||||
|
||||
/**
|
||||
* Determine whether a sequence of letters exists in another string,
|
||||
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
|
||||
*
|
||||
* @param {string} filter - Sequence of letters to check for
|
||||
* @param {string} word - Word to check for sequence
|
||||
*
|
||||
* @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
|
||||
*/
|
||||
|
||||
export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
|
||||
let topScore = 0;
|
||||
|
||||
for (const word of words) {
|
||||
const scores = fuzzyScore(
|
||||
filter,
|
||||
filter.toLowerCase(),
|
||||
0,
|
||||
word,
|
||||
word.toLowerCase(),
|
||||
0,
|
||||
true
|
||||
);
|
||||
|
||||
if (!scores) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The VS Code implementation of filter treats a score of "0" as just barely a match
|
||||
// But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
|
||||
// By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
|
||||
const score = scores[0] + 1;
|
||||
|
||||
if (score > topScore) {
|
||||
topScore = score;
|
||||
}
|
||||
}
|
||||
return topScore;
|
||||
};
|
||||
|
||||
export interface ScorableTextItem {
|
||||
score?: number;
|
||||
text: string;
|
||||
altText?: string;
|
||||
}
|
||||
|
||||
type FuzzyFilterSort = <T extends ScorableTextItem>(
|
||||
filter: string,
|
||||
items: T[]
|
||||
) => T[];
|
||||
|
||||
export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
|
||||
return items
|
||||
.map((item) => {
|
||||
item.score = item.altText
|
||||
? fuzzySequentialMatch(filter, item.text, item.altText)
|
||||
: fuzzySequentialMatch(filter, item.text);
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item.score !== undefined && item.score > 0)
|
||||
.sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
|
||||
scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
|
||||
*
|
||||
* @param num The number to format
|
||||
* @param language The language to use when formatting the number
|
||||
*/
|
||||
export const numberFormat = (
|
||||
num: string | number,
|
||||
language: string
|
||||
): string => {
|
||||
// Polyfill for Number.isNaN, which is more reliable that the global isNaN()
|
||||
Number.isNaN =
|
||||
Number.isNaN ||
|
||||
function isNaN(input) {
|
||||
return typeof input === "number" && isNaN(input);
|
||||
};
|
||||
|
||||
if (!Number.isNaN(Number(num)) && Intl) {
|
||||
return new Intl.NumberFormat(language).format(Number(num));
|
||||
}
|
||||
return num.toString();
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Determine whether a sequence of letters exists in another string,
|
||||
* in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
|
||||
*
|
||||
* filter => sequence of letters
|
||||
* word => Word to check for sequence
|
||||
*
|
||||
* return true if word contains sequence. Otherwise false.
|
||||
*/
|
||||
export const fuzzySequentialMatch = (filter: string, word: string) => {
|
||||
if (filter === "") {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let i = 0; i <= filter.length; i++) {
|
||||
const pos = word.indexOf(filter[0]);
|
||||
|
||||
if (pos < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newWord = word.substring(pos + 1);
|
||||
const newFilter = filter.substring(1);
|
||||
|
||||
return fuzzySequentialMatch(newFilter, newWord);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import IntlMessageFormat from "intl-messageformat";
|
||||
import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
|
||||
import { Resources } from "../../types";
|
||||
|
||||
export type LocalizeFunc = (key: string, ...args: any[]) => string;
|
||||
@@ -12,9 +13,12 @@ export interface FormatsType {
|
||||
time: FormatType;
|
||||
}
|
||||
|
||||
if (!Intl.PluralRules) {
|
||||
import("@formatjs/intl-pluralrules/polyfill-locales");
|
||||
}
|
||||
let polyfillLoaded = !shouldPolyfill();
|
||||
const polyfillProm = polyfillLoaded
|
||||
? undefined
|
||||
: import("@formatjs/intl-pluralrules/polyfill-locales").then(() => {
|
||||
polyfillLoaded = true;
|
||||
});
|
||||
|
||||
/**
|
||||
* Adapted from Polymer app-localize-behavior.
|
||||
@@ -37,12 +41,16 @@ if (!Intl.PluralRules) {
|
||||
* }
|
||||
*/
|
||||
|
||||
export const computeLocalize = (
|
||||
export const computeLocalize = async (
|
||||
cache: any,
|
||||
language: string,
|
||||
resources: Resources,
|
||||
formats?: FormatsType
|
||||
): LocalizeFunc => {
|
||||
): Promise<LocalizeFunc> => {
|
||||
if (!polyfillLoaded) {
|
||||
await polyfillProm;
|
||||
}
|
||||
|
||||
// Everytime any of the parameters change, invalidate the strings cache.
|
||||
cache._localizationCache = {};
|
||||
|
||||
|
||||
@@ -94,6 +94,8 @@ export class HaDataTable extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public selectable = false;
|
||||
|
||||
@property({ type: Boolean }) public clickable = false;
|
||||
|
||||
@property({ type: Boolean }) public hasFab = false;
|
||||
|
||||
@property({ type: Boolean, attribute: "auto-height" })
|
||||
@@ -327,12 +329,13 @@ export class HaDataTable extends LitElement {
|
||||
<div
|
||||
aria-rowindex=${index}
|
||||
role="row"
|
||||
.rowId="${row[this.id]}"
|
||||
.rowId=${row[this.id]}
|
||||
@click=${this._handleRowClick}
|
||||
class="mdc-data-table__row ${classMap({
|
||||
"mdc-data-table__row--selected": this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
),
|
||||
clickable: this.clickable,
|
||||
})}"
|
||||
aria-selected=${ifDefined(
|
||||
this._checkedRows.includes(String(row[this.id]))
|
||||
@@ -350,6 +353,7 @@ export class HaDataTable extends LitElement {
|
||||
<ha-checkbox
|
||||
class="mdc-data-table__row-checkbox"
|
||||
@change=${this._handleRowCheckboxClick}
|
||||
.rowId=${row[this.id]}
|
||||
.disabled=${row.selectable === false}
|
||||
.checked=${this._checkedRows.includes(
|
||||
String(row[this.id])
|
||||
@@ -457,9 +461,7 @@ export class HaDataTable extends LitElement {
|
||||
);
|
||||
|
||||
private _handleHeaderClick(ev: Event) {
|
||||
const columnId = ((ev.target as HTMLElement).closest(
|
||||
".mdc-data-table__header-cell"
|
||||
) as any).columnId;
|
||||
const columnId = (ev.currentTarget as any).columnId;
|
||||
if (!this.columns[columnId].sortable) {
|
||||
return;
|
||||
}
|
||||
@@ -493,8 +495,8 @@ export class HaDataTable extends LitElement {
|
||||
}
|
||||
|
||||
private _handleRowCheckboxClick(ev: Event) {
|
||||
const checkbox = ev.target as HaCheckbox;
|
||||
const rowId = (checkbox.closest(".mdc-data-table__row") as any).rowId;
|
||||
const checkbox = ev.currentTarget as HaCheckbox;
|
||||
const rowId = (checkbox as any).rowId;
|
||||
|
||||
if (checkbox.checked) {
|
||||
if (this._checkedRows.includes(rowId)) {
|
||||
@@ -512,7 +514,7 @@ export class HaDataTable extends LitElement {
|
||||
if (target.tagName === "HA-CHECKBOX") {
|
||||
return;
|
||||
}
|
||||
const rowId = (target.closest(".mdc-data-table__row") as any).rowId;
|
||||
const rowId = (ev.currentTarget as any).rowId;
|
||||
fireEvent(this, "row-click", { id: rowId }, { bubbles: false });
|
||||
}
|
||||
|
||||
@@ -886,6 +888,9 @@ export class HaDataTable extends LitElement {
|
||||
.forceLTR {
|
||||
direction: ltr;
|
||||
}
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
|
||||
@customElement("ha-device-action-picker")
|
||||
class HaDeviceActionPicker extends HaDeviceAutomationPicker<DeviceAction> {
|
||||
protected NO_AUTOMATION_TEXT = "No actions";
|
||||
protected get NO_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.no_actions"
|
||||
);
|
||||
}
|
||||
|
||||
protected UNKNOWN_AUTOMATION_TEXT = "Unknown action";
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
|
||||
@@ -33,16 +33,24 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
@property() public value?: T;
|
||||
|
||||
protected NO_AUTOMATION_TEXT = "No automations";
|
||||
|
||||
protected UNKNOWN_AUTOMATION_TEXT = "Unknown automation";
|
||||
|
||||
@internalProperty() private _automations: T[] = [];
|
||||
|
||||
// Trigger an empty render so we start with a clean DOM.
|
||||
// paper-listbox does not like changing things around.
|
||||
@internalProperty() private _renderEmpty = false;
|
||||
|
||||
protected get NO_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.no_actions"
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
);
|
||||
}
|
||||
|
||||
private _localizeDeviceAutomation: (
|
||||
hass: HomeAssistant,
|
||||
automation: T
|
||||
|
||||
@@ -11,9 +11,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
class HaDeviceConditionPicker extends HaDeviceAutomationPicker<
|
||||
DeviceCondition
|
||||
> {
|
||||
protected NO_AUTOMATION_TEXT = "No conditions";
|
||||
protected get NO_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.conditions.no_conditions"
|
||||
);
|
||||
}
|
||||
|
||||
protected UNKNOWN_AUTOMATION_TEXT = "Unknown condition";
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.conditions.unknown_condition"
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
|
||||
@@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
|
||||
|
||||
@customElement("ha-device-trigger-picker")
|
||||
class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
|
||||
protected NO_AUTOMATION_TEXT = "No triggers";
|
||||
protected get NO_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.triggers.no_triggers"
|
||||
);
|
||||
}
|
||||
|
||||
protected UNKNOWN_AUTOMATION_TEXT = "Unknown trigger";
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.devices.automation.triggers.unknown_trigger"
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
|
||||
@@ -88,6 +88,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
.chartTooltip .beforeBody {
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
word-break: break-all;
|
||||
}
|
||||
.chartLegend li {
|
||||
display: inline-block;
|
||||
@@ -278,7 +279,7 @@ class HaChartBase extends mixinBehaviors(
|
||||
this.set(["tooltip", "title"], title);
|
||||
|
||||
if (tooltip.beforeBody) {
|
||||
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
|
||||
this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
|
||||
}
|
||||
|
||||
const bodyLines = tooltip.body.map((n) => n.lines);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-item";
|
||||
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
|
||||
@@ -16,8 +17,9 @@ import {
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
|
||||
@@ -80,6 +82,7 @@ class HaEntityAttributePicker extends LitElement {
|
||||
.value=${this._value}
|
||||
.allowCustomValue=${this.allowCustomValue}
|
||||
.renderer=${rowRenderer}
|
||||
attr-for-value="bind-value"
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -97,33 +100,35 @@ class HaEntityAttributePicker extends LitElement {
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
${this.value
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="suffix" slot="suffix">
|
||||
${this.value
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.clear"
|
||||
)}
|
||||
class="clear-button"
|
||||
tabindex="-1"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.entity.entity-attribute-picker.show_attributes"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
>
|
||||
Toggle
|
||||
</ha-icon-button>
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.entity.entity-attribute-picker.show_attributes"
|
||||
)}
|
||||
class="toggle-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</div>
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
`;
|
||||
@@ -159,7 +164,10 @@ class HaEntityAttributePicker extends LitElement {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > ha-icon-button {
|
||||
.suffix {
|
||||
display: flex;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 0px 2px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import "@material/mwc-icon-button/mwc-icon-button";
|
||||
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@polymer/paper-item/paper-icon-item";
|
||||
import "@polymer/paper-item/paper-item-body";
|
||||
@@ -20,7 +22,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { HomeAssistant } from "../../types";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
|
||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
|
||||
@@ -101,6 +103,8 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
private _initedStates = false;
|
||||
|
||||
private _states: HassEntity[] = [];
|
||||
|
||||
private _getStates = memoizeOne(
|
||||
(
|
||||
_opened: boolean,
|
||||
@@ -166,7 +170,7 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
|
||||
const states = this._getStates(
|
||||
this._states = this._getStates(
|
||||
this._opened,
|
||||
this.hass,
|
||||
this.includeDomains,
|
||||
@@ -174,7 +178,7 @@ export class HaEntityPicker extends LitElement {
|
||||
this.entityFilter,
|
||||
this.includeDeviceClasses
|
||||
);
|
||||
(this._comboBox as any).items = states;
|
||||
(this._comboBox as any).filteredItems = this._states;
|
||||
this._initedStates = true;
|
||||
}
|
||||
}
|
||||
@@ -192,6 +196,7 @@ export class HaEntityPicker extends LitElement {
|
||||
.renderer=${rowRenderer}
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._valueChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
>
|
||||
<paper-input
|
||||
.autofocus=${this.autofocus}
|
||||
@@ -206,35 +211,35 @@ export class HaEntityPicker extends LitElement {
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
>
|
||||
${this.value && !this.hideClearIcon
|
||||
? html`
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.clear"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="clear-button"
|
||||
icon="hass:close"
|
||||
tabindex="-1"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
Clear
|
||||
</ha-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="suffix" slot="suffix">
|
||||
${this.value && !this.hideClearIcon
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.clear"
|
||||
)}
|
||||
class="clear-button"
|
||||
tabindex="-1"
|
||||
@click=${this._clearValue}
|
||||
no-ripple
|
||||
>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
|
||||
<ha-icon-button
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.show_entities"
|
||||
)}
|
||||
slot="suffix"
|
||||
class="toggle-button"
|
||||
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
|
||||
tabindex="-1"
|
||||
>
|
||||
Toggle
|
||||
</ha-icon-button>
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.entity.entity-picker.show_entities"
|
||||
)}
|
||||
class="toggle-button"
|
||||
tabindex="-1"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</div>
|
||||
</paper-input>
|
||||
</vaadin-combo-box-light>
|
||||
`;
|
||||
@@ -260,6 +265,15 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
(this._comboBox as any).filteredItems = this._states.filter(
|
||||
(state) =>
|
||||
state.entity_id.toLowerCase().includes(filterString) ||
|
||||
computeStateName(state).toLowerCase().includes(filterString)
|
||||
);
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
this.value = value;
|
||||
setTimeout(() => {
|
||||
@@ -270,7 +284,10 @@ export class HaEntityPicker extends LitElement {
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
paper-input > ha-icon-button {
|
||||
.suffix {
|
||||
display: flex;
|
||||
}
|
||||
mwc-icon-button {
|
||||
--mdc-icon-button-size: 24px;
|
||||
padding: 0px 2px;
|
||||
color: var(--secondary-text-color);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { stateIcon } from "../../common/entity/state_icon";
|
||||
import "../ha-icon";
|
||||
|
||||
class HaStateIcon extends PolymerElement {
|
||||
static get template() {
|
||||
return html` <ha-icon icon="[[computeIcon(stateObj)]]"></ha-icon> `;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
stateObj: {
|
||||
type: Object,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeIcon(stateObj) {
|
||||
return stateIcon(stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-state-icon", HaStateIcon);
|
||||
@@ -11,11 +11,14 @@ import {
|
||||
} from "lit-element";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
|
||||
import { computeActiveState } from "../../common/entity/compute_active_state";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { stateIcon } from "../../common/entity/state_icon";
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
import "../ha-icon";
|
||||
|
||||
export class StateBadge extends LitElement {
|
||||
@@ -37,7 +40,13 @@ export class StateBadge extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
if (!stateObj || !this._showIcon) {
|
||||
if (!stateObj) {
|
||||
return html`<div class="missing">
|
||||
<ha-icon icon="hass:alert"></ha-icon>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (!this._showIcon) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
@@ -140,6 +149,9 @@ export class StateBadge extends LitElement {
|
||||
ha-icon {
|
||||
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
|
||||
}
|
||||
.missing {
|
||||
color: #fce588;
|
||||
}
|
||||
|
||||
${iconColorCSS}
|
||||
`;
|
||||
|
||||
@@ -59,6 +59,19 @@ class StateInfo extends LocalizeMixin(PolymerElement) {
|
||||
@apply --paper-font-common-nowrap;
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: no-wrap;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
margin: 0 2px 4px 0;
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
}
|
||||
@@ -81,11 +94,26 @@ class StateInfo extends LocalizeMixin(PolymerElement) {
|
||||
datetime="[[stateObj.last_changed]]"
|
||||
></ha-relative-time>
|
||||
<paper-tooltip animation-delay="0" for="last_changed">
|
||||
[[localize('ui.dialogs.more_info_control.last_updated')]]:
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[stateObj.last_updated]]"
|
||||
></ha-relative-time>
|
||||
<div>
|
||||
<div class="row">
|
||||
<span class="column-name">
|
||||
[[localize('ui.dialogs.more_info_control.last_changed')]]:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[stateObj.last_changed]]"
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span>
|
||||
[[localize('ui.dialogs.more_info_control.last_updated')]]:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
hass="[[hass]]"
|
||||
datetime="[[stateObj.last_updated]]"
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
</div>
|
||||
</paper-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -34,7 +34,7 @@ class HaAttributes extends LitElement {
|
||||
(attribute) => html`
|
||||
<div class="data-entry">
|
||||
<div class="key">
|
||||
${attribute.replace(/_/g, " ").replace("id", "ID")}
|
||||
${attribute.replace(/_/g, " ").replace(/\bid\b/g, "ID")}
|
||||
</div>
|
||||
<div class="value">
|
||||
${this.formatAttribute(attribute)}
|
||||
@@ -63,13 +63,14 @@ class HaAttributes extends LitElement {
|
||||
.data-entry .value {
|
||||
max-width: 200px;
|
||||
overflow-wrap: break-word;
|
||||
text-align: right;
|
||||
}
|
||||
.key:first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.attribution {
|
||||
color: var(--secondary-text-color);
|
||||
text-align: right;
|
||||
text-align: center;
|
||||
}
|
||||
pre {
|
||||
font-family: inherit;
|
||||
|
||||
@@ -50,9 +50,12 @@ export class HaCard extends LitElement {
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, 24px);
|
||||
letter-spacing: -0.012em;
|
||||
line-height: 32px;
|
||||
padding: 24px 16px 16px;
|
||||
line-height: 48px;
|
||||
padding: 12px 16px 16px;
|
||||
display: block;
|
||||
margin-block-start: 0px;
|
||||
margin-block-end: 0px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
:host ::slotted(.card-content:not(:first-child)),
|
||||
@@ -75,7 +78,7 @@ export class HaCard extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.header
|
||||
? html` <div class="card-header">${this.header}</div> `
|
||||
? html`<h1 class="card-header">${this.header}</h1>`
|
||||
: html``}
|
||||
<slot></slot>
|
||||
`;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaClimateState extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.current {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.state-label {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.unit {
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="target">
|
||||
<template is="dom-if" if="[[_hasKnownState(stateObj.state)]]">
|
||||
<span class="state-label">
|
||||
[[_localizeState(localize, stateObj)]]
|
||||
<template is="dom-if" if="[[_renderPreset(stateObj.attributes)]]">
|
||||
- [[_localizePreset(localize, stateObj.attributes.preset_mode)]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<div class="unit">[[computeTarget(hass, stateObj)]]</div>
|
||||
</div>
|
||||
|
||||
<template is="dom-if" if="[[currentStatus]]">
|
||||
<div class="current">
|
||||
[[localize('ui.card.climate.currently')]]:
|
||||
<div class="unit">[[currentStatus]]</div>
|
||||
</div>
|
||||
</template>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
hass: Object,
|
||||
stateObj: Object,
|
||||
currentStatus: {
|
||||
type: String,
|
||||
computed: "computeCurrentStatus(hass, stateObj)",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
computeCurrentStatus(hass, stateObj) {
|
||||
if (!hass || !stateObj) return null;
|
||||
if (stateObj.attributes.current_temperature != null) {
|
||||
return `${stateObj.attributes.current_temperature} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.current_humidity != null) {
|
||||
return `${stateObj.attributes.current_humidity} %`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
computeTarget(hass, stateObj) {
|
||||
if (!hass || !stateObj) return null;
|
||||
// We're using "!= null" on purpose so that we match both null and undefined.
|
||||
if (
|
||||
stateObj.attributes.target_temp_low != null &&
|
||||
stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${stateObj.attributes.target_temp_low}-${stateObj.attributes.target_temp_high} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (stateObj.attributes.temperature != null) {
|
||||
return `${stateObj.attributes.temperature} ${hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (
|
||||
stateObj.attributes.target_humidity_low != null &&
|
||||
stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${stateObj.attributes.target_humidity_low}-${stateObj.attributes.target_humidity_high}%`;
|
||||
}
|
||||
if (stateObj.attributes.humidity != null) {
|
||||
return `${stateObj.attributes.humidity} %`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
_hasKnownState(state) {
|
||||
return state !== "unknown";
|
||||
}
|
||||
|
||||
_localizeState(localize, stateObj) {
|
||||
const stateString = localize(`component.climate.state._.${stateObj.state}`);
|
||||
return stateObj.attributes.hvac_action
|
||||
? `${localize(
|
||||
`state_attributes.climate.hvac_action.${stateObj.attributes.hvac_action}`
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
}
|
||||
|
||||
_localizePreset(localize, preset) {
|
||||
return localize(`state_attributes.climate.preset_mode.${preset}`) || preset;
|
||||
}
|
||||
|
||||
_renderPreset(attributes) {
|
||||
return (
|
||||
attributes.preset_mode && attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
);
|
||||
}
|
||||
}
|
||||
customElements.define("ha-climate-state", HaClimateState);
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
|
||||
import { CLIMATE_PRESET_NONE } from "../data/climate";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-climate-state")
|
||||
class HaClimateState extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const currentStatus = this._computeCurrentStatus();
|
||||
|
||||
return html`<div class="target">
|
||||
${this.stateObj.state !== "unknown"
|
||||
? html`<span class="state-label">
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.preset_mode &&
|
||||
this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`-
|
||||
${this.hass.localize(
|
||||
`state_attributes.climate.preset_mode.${this.stateObj.attributes.preset_mode}`
|
||||
) || this.stateObj.attributes.preset_mode}`
|
||||
: ""}
|
||||
</span>`
|
||||
: ""}
|
||||
<div class="unit">${this._computeTarget()}</div>
|
||||
</div>
|
||||
|
||||
${currentStatus
|
||||
? html`<div class="current">
|
||||
${this.hass.localize("ui.card.climate.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
</div>`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private _computeCurrentStatus(): string | undefined {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_temperature != null) {
|
||||
return `${this.stateObj.attributes.current_temperature} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${this.stateObj.attributes.current_humidity} %`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _computeTarget(): string {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (
|
||||
this.stateObj.attributes.target_temp_low != null &&
|
||||
this.stateObj.attributes.target_temp_high != null
|
||||
) {
|
||||
return `${this.stateObj.attributes.target_temp_low}-${this.stateObj.attributes.target_temp_high} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.temperature != null) {
|
||||
return `${this.stateObj.attributes.temperature} ${this.hass.config.unit_system.temperature}`;
|
||||
}
|
||||
if (
|
||||
this.stateObj.attributes.target_humidity_low != null &&
|
||||
this.stateObj.attributes.target_humidity_high != null
|
||||
) {
|
||||
return `${this.stateObj.attributes.target_humidity_low}-${this.stateObj.attributes.target_humidity_high}%`;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${this.stateObj.attributes.humidity} %`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private _localizeState(): string {
|
||||
const stateString = this.hass.localize(
|
||||
`component.climate.state._.${this.stateObj.state}`
|
||||
);
|
||||
|
||||
return this.stateObj.attributes.hvac_action
|
||||
? `${this.hass.localize(
|
||||
`state_attributes.climate.hvac_action.${this.stateObj.attributes.hvac_action}`
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.current {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.state-label {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.unit {
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-climate-state": HaClimateState;
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,7 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._blockKeyboardShortcuts();
|
||||
this._load();
|
||||
}
|
||||
|
||||
@@ -232,6 +233,10 @@ export class HaCodeEditor extends UpdatingElement {
|
||||
this.codemirror!.on("changes", () => this._onChange());
|
||||
}
|
||||
|
||||
private _blockKeyboardShortcuts() {
|
||||
this.addEventListener("keydown", (ev) => ev.stopPropagation());
|
||||
}
|
||||
|
||||
private _onChange(): void {
|
||||
const newValue = this.value;
|
||||
if (newValue === this._value) {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
query,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-svg-icon";
|
||||
import { mdiChevronDown } from "@mdi/js";
|
||||
import { classMap } from "lit-html/directives/class-map";
|
||||
|
||||
@customElement("ha-expansion-panel")
|
||||
class HaExpansionPanel extends LitElement {
|
||||
@property({ type: Boolean, reflect: true }) expanded = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) outlined = false;
|
||||
|
||||
@query(".container") private _container!: HTMLDivElement;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="summary" @click=${this._toggleContainer}>
|
||||
<slot name="title"></slot>
|
||||
<ha-svg-icon
|
||||
.path=${mdiChevronDown}
|
||||
class="summary-icon ${classMap({ expanded: this.expanded })}"
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
<div
|
||||
class="container ${classMap({ expanded: this.expanded })}"
|
||||
@transitionend=${this._handleTransitionEnd}
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleTransitionEnd() {
|
||||
this._container.style.removeProperty("height");
|
||||
}
|
||||
|
||||
private _toggleContainer(): void {
|
||||
const scrollHeight = this._container.scrollHeight;
|
||||
this._container.style.height = `${scrollHeight}px`;
|
||||
|
||||
if (this.expanded) {
|
||||
setTimeout(() => {
|
||||
this._container.style.height = "0px";
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.expanded = !this.expanded;
|
||||
fireEvent(this, "expanded-changed", { expanded: this.expanded });
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:host([outlined]) {
|
||||
box-shadow: none;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: var(
|
||||
--ha-card-border-color,
|
||||
var(--divider-color, #e0e0e0)
|
||||
);
|
||||
border-radius: var(--ha-card-border-radius, 4px);
|
||||
}
|
||||
|
||||
.summary {
|
||||
display: flex;
|
||||
padding: 0px 16px;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summary-icon {
|
||||
transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.summary-icon.expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.container {
|
||||
overflow: hidden;
|
||||
transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.container.expanded {
|
||||
height: auto;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-expansion-panel": HaExpansionPanel;
|
||||
}
|
||||
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"expanded-changed": {
|
||||
expanded: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,8 @@ class HaHLSPlayer extends LitElement {
|
||||
@property({ type: Boolean, attribute: "allow-exoplayer" })
|
||||
public allowExoPlayer = false;
|
||||
|
||||
@query("video", true) private _videoEl!: HTMLVideoElement;
|
||||
// don't cache this, as we remove it on disconnects
|
||||
@query("video") private _videoEl!: HTMLVideoElement;
|
||||
|
||||
@internalProperty() private _attached = false;
|
||||
|
||||
@@ -154,6 +155,9 @@ class HaHLSPlayer extends LitElement {
|
||||
}
|
||||
|
||||
private _resizeExoPlayer = () => {
|
||||
if (!this._videoEl) {
|
||||
return;
|
||||
}
|
||||
const rect = this._videoEl.getBoundingClientRect();
|
||||
this.hass!.auth.external!.fireMessage({
|
||||
type: "exoplayer/resize",
|
||||
|
||||
@@ -14,8 +14,8 @@ class HaLabeledSlider extends PolymerElement {
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 16px;
|
||||
color: var(--secondary-text-color);
|
||||
margin-bottom: 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
@@ -43,7 +43,6 @@ class HaLabeledSlider extends PolymerElement {
|
||||
step="[[step]]"
|
||||
pin="[[pin]]"
|
||||
disabled="[[disabled]]"
|
||||
disabled="[[disabled]]"
|
||||
value="{{value}}"
|
||||
></ha-slider>
|
||||
</div>
|
||||
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
internalProperty,
|
||||
LitElement,
|
||||
property,
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -98,8 +98,7 @@ class HaMenuButton extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
this.style.visibility =
|
||||
newNarrow || this._alwaysVisible ? "initial" : "hidden";
|
||||
this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
|
||||
|
||||
if (!newNarrow) {
|
||||
this._hasNotifications = false;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { getAppKey } from "../data/notify_html5";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
import { showPromptDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import "./ha-switch";
|
||||
|
||||
export const pushSupported =
|
||||
@@ -88,7 +89,14 @@ class HaPushNotificationsToggle extends EventsMixin(PolymerElement) {
|
||||
browserName = "chrome";
|
||||
}
|
||||
|
||||
const name = prompt("What should this device be called ?");
|
||||
const name = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.profile.push_notifications.add_device_prompt.title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.profile.push_notifications.add_device_prompt.input_label"
|
||||
),
|
||||
});
|
||||
if (name == null) {
|
||||
this.pushChecked = false;
|
||||
return;
|
||||
|
||||
+223
-189
@@ -202,195 +202,17 @@ class HaSidebar extends LitElement {
|
||||
private _sortable?;
|
||||
|
||||
protected render() {
|
||||
const hass = this.hass;
|
||||
|
||||
if (!hass) {
|
||||
if (!this.hass) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
hass.panels,
|
||||
hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
let notificationCount = this._notifications
|
||||
? this._notifications.length
|
||||
: 0;
|
||||
for (const entityId in hass.states) {
|
||||
if (computeDomain(entityId) === "configurator") {
|
||||
notificationCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<div
|
||||
class="menu"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this.editMode,
|
||||
disabled: this.editMode,
|
||||
})}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
.label=${hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
@action=${this._toggleSidebar}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${hass.dockedSidebar === "docked"
|
||||
? mdiMenuOpen
|
||||
: mdiMenu}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="title">
|
||||
${this.editMode
|
||||
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||
${hass.localize("ui.sidebar.done")}
|
||||
</mwc-button>`
|
||||
: "Home Assistant"}
|
||||
</div>
|
||||
</div>
|
||||
<paper-listbox
|
||||
attr-for-selected="data-panel"
|
||||
class="ha-scrollbar"
|
||||
.selected=${hass.panelUrl}
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this.editMode
|
||||
? html`<div id="sortable">
|
||||
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
||||
this._renderEmptySortable
|
||||
? ""
|
||||
: this._renderPanels(beforeSpacer)
|
||||
)}
|
||||
</div>`
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
<div class="spacer" disabled></div>
|
||||
${this.editMode && this._hiddenPanels.length
|
||||
? html`
|
||||
${this._hiddenPanels.map((url) => {
|
||||
const panel = this.hass.panels[url];
|
||||
if (!panel) {
|
||||
return "";
|
||||
}
|
||||
return html`<paper-icon-item
|
||||
@click=${this._unhidePanel}
|
||||
class="hidden-panel"
|
||||
.panel=${url}
|
||||
>
|
||||
<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${panel.url_path === this.hass.defaultPanel
|
||||
? "mdi:view-dashboard"
|
||||
: panel.icon}
|
||||
></ha-icon>
|
||||
<span class="item-text"
|
||||
>${panel.url_path === this.hass.defaultPanel
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title}</span
|
||||
>
|
||||
<mwc-icon-button class="show-panel">
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-icon-item>`;
|
||||
})}
|
||||
<div class="spacer" disabled></div>
|
||||
`
|
||||
: ""}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
aria-role="option"
|
||||
aria-label=${hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
href="#external-app-configuration"
|
||||
tabindex="-1"
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiCellphoneCog}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
: ""}
|
||||
</paper-listbox>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<div
|
||||
class="notifications-container"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item
|
||||
class="notifications"
|
||||
aria-role="option"
|
||||
@click=${this._handleShowNotificationDrawer}
|
||||
>
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
|
||||
${!this.expanded && notificationCount > 0
|
||||
? html`
|
||||
<span class="notification-badge" slot="item-icon">
|
||||
${notificationCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text">
|
||||
${hass.localize("ui.notification_drawer.title")}
|
||||
</span>
|
||||
${this.expanded && notificationCount > 0
|
||||
? html`
|
||||
<span class="notification-badge">${notificationCount}</span>
|
||||
`
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class=${classMap({
|
||||
profile: true,
|
||||
// Mimick behavior that paper-listbox provides
|
||||
"iron-selected": hass.panelUrl === "profile",
|
||||
})}
|
||||
href="/profile"
|
||||
data-panel="panel"
|
||||
tabindex="-1"
|
||||
aria-role="option"
|
||||
aria-label=${hass.localize("panel.profile")}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-user-badge
|
||||
slot="item-icon"
|
||||
.user=${hass.user}
|
||||
.hass=${hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text">
|
||||
${hass.user ? hass.user.name : ""}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
${this._renderHeader()}
|
||||
${this._renderAllPanels()}
|
||||
${this._renderDivider()}
|
||||
${this._renderNotifications()}
|
||||
${this._renderUserItem()}
|
||||
<div disabled class="bottom-spacer"></div>
|
||||
<div class="tooltip"></div>
|
||||
`;
|
||||
@@ -475,6 +297,215 @@ class HaSidebar extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _renderHeader() {
|
||||
return html`<div
|
||||
class="menu"
|
||||
@action=${this._handleAction}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this.editMode,
|
||||
disabled: this.editMode,
|
||||
})}
|
||||
>
|
||||
${!this.narrow
|
||||
? html`
|
||||
<mwc-icon-button
|
||||
.label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
|
||||
@action=${this._toggleSidebar}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${this.hass.dockedSidebar === "docked"
|
||||
? mdiMenuOpen
|
||||
: mdiMenu}
|
||||
></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
`
|
||||
: ""}
|
||||
<div class="title">
|
||||
${this.editMode
|
||||
? html`<mwc-button outlined @click=${this._closeEditMode}>
|
||||
${this.hass.localize("ui.sidebar.done")}
|
||||
</mwc-button>`
|
||||
: "Home Assistant"}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderAllPanels() {
|
||||
const [beforeSpacer, afterSpacer] = computePanels(
|
||||
this.hass.panels,
|
||||
this.hass.defaultPanel,
|
||||
this._panelOrder,
|
||||
this._hiddenPanels
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<paper-listbox
|
||||
attr-for-selected="data-panel"
|
||||
class="ha-scrollbar"
|
||||
.selected=${this.hass.panelUrl}
|
||||
@focusin=${this._listboxFocusIn}
|
||||
@focusout=${this._listboxFocusOut}
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this.editMode
|
||||
? this._renderPanelsEdit(beforeSpacer)
|
||||
: this._renderPanels(beforeSpacer)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer)}
|
||||
${this._renderExternalConfiguration()}
|
||||
</paper-listbox>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanelsEdit(beforeSpacer: PanelInfo[]) {
|
||||
// prettier-ignore
|
||||
return html`<div id="sortable">
|
||||
${guard([this._hiddenPanels, this._renderEmptySortable], () =>
|
||||
this._renderEmptySortable ? "" : this._renderPanels(beforeSpacer)
|
||||
)}
|
||||
</div>
|
||||
${this._renderSpacer()}
|
||||
${this._renderHiddenPanels()} `;
|
||||
}
|
||||
|
||||
private _renderHiddenPanels() {
|
||||
return html` ${this._hiddenPanels.length
|
||||
? html`${this._hiddenPanels.map((url) => {
|
||||
const panel = this.hass.panels[url];
|
||||
if (!panel) {
|
||||
return "";
|
||||
}
|
||||
return html`<paper-icon-item
|
||||
@click=${this._unhidePanel}
|
||||
class="hidden-panel"
|
||||
.panel=${url}
|
||||
>
|
||||
<ha-icon
|
||||
slot="item-icon"
|
||||
.icon=${panel.url_path === this.hass.defaultPanel
|
||||
? "mdi:view-dashboard"
|
||||
: panel.icon}
|
||||
></ha-icon>
|
||||
<span class="item-text"
|
||||
>${panel.url_path === this.hass.defaultPanel
|
||||
? this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title}</span
|
||||
>
|
||||
<mwc-icon-button class="show-panel">
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</paper-icon-item>`;
|
||||
})}
|
||||
${this._renderSpacer()}`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private _renderDivider() {
|
||||
return html`<div class="divider"></div>`;
|
||||
}
|
||||
|
||||
private _renderSpacer() {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderNotifications() {
|
||||
let notificationCount = this._notifications
|
||||
? this._notifications.length
|
||||
: 0;
|
||||
for (const entityId in this.hass.states) {
|
||||
if (computeDomain(entityId) === "configurator") {
|
||||
notificationCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return html` <div
|
||||
class="notifications-container"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item
|
||||
class="notifications"
|
||||
aria-role="option"
|
||||
@click=${this._handleShowNotificationDrawer}
|
||||
>
|
||||
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
|
||||
${!this.expanded && notificationCount > 0
|
||||
? html`
|
||||
<span class="notification-badge" slot="item-icon">
|
||||
${notificationCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.notification_drawer.title")}
|
||||
</span>
|
||||
${this.expanded && notificationCount > 0
|
||||
? html` <span class="notification-badge">${notificationCount}</span> `
|
||||
: ""}
|
||||
</paper-icon-item>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderUserItem() {
|
||||
return html`<a
|
||||
class=${classMap({
|
||||
profile: true,
|
||||
// Mimick behavior that paper-listbox provides
|
||||
"iron-selected": this.hass.panelUrl === "profile",
|
||||
})}
|
||||
href="/profile"
|
||||
data-panel="panel"
|
||||
tabindex="-1"
|
||||
aria-role="option"
|
||||
aria-label=${this.hass.localize("panel.profile")}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-user-badge
|
||||
slot="item-icon"
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text">
|
||||
${this.hass.user ? this.hass.user.name : ""}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${this._externalConfig && this._externalConfig.hasSettingsScreen
|
||||
? html`
|
||||
<a
|
||||
aria-role="option"
|
||||
aria-label=${this.hass.localize(
|
||||
"ui.sidebar.external_app_configuration"
|
||||
)}
|
||||
href="#external-app-configuration"
|
||||
tabindex="-1"
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<paper-icon-item>
|
||||
<ha-svg-icon
|
||||
slot="item-icon"
|
||||
.path=${mdiCellphoneCog}
|
||||
></ha-svg-icon>
|
||||
<span class="item-text">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</paper-icon-item>
|
||||
</a>
|
||||
`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private get _tooltip() {
|
||||
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
|
||||
}
|
||||
@@ -728,6 +759,7 @@ class HaSidebar extends LitElement {
|
||||
width: 64px;
|
||||
}
|
||||
:host([expanded]) {
|
||||
width: 256px;
|
||||
width: calc(256px + env(safe-area-inset-left));
|
||||
}
|
||||
:host([rtl]) {
|
||||
@@ -735,8 +767,7 @@ class HaSidebar extends LitElement {
|
||||
border-left: 1px solid var(--divider-color);
|
||||
}
|
||||
.menu {
|
||||
box-sizing: border-box;
|
||||
height: 65px;
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
padding: 0 8.5px;
|
||||
border-bottom: 1px solid transparent;
|
||||
@@ -781,7 +812,7 @@ class HaSidebar extends LitElement {
|
||||
display: initial;
|
||||
}
|
||||
.title mwc-button {
|
||||
width: 100%;
|
||||
width: 90%;
|
||||
}
|
||||
#sortable,
|
||||
.hidden-panel {
|
||||
@@ -793,7 +824,10 @@ class HaSidebar extends LitElement {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
height: calc(100% - 196px - env(safe-area-inset-bottom));
|
||||
height: calc(100% - var(--header-height) - 132px);
|
||||
height: calc(
|
||||
100% - var(--header-height) - 132px - env(safe-area-inset-bottom)
|
||||
);
|
||||
overflow-x: hidden;
|
||||
background: none;
|
||||
margin-left: env(safe-area-inset-left);
|
||||
|
||||
@@ -21,6 +21,7 @@ class HaSlider extends PaperSliderClass {
|
||||
.pin > .slider-knob > .slider-knob-inner {
|
||||
font-size: var(--ha-slider-pin-font-size, 10px);
|
||||
line-height: normal;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.disabled.ring > .slider-knob > .slider-knob-inner {
|
||||
@@ -69,9 +70,9 @@ class HaSlider extends PaperSliderClass {
|
||||
transform: scale(1) translate(0, -10px);
|
||||
}
|
||||
|
||||
.slider-input {
|
||||
width: 54px;
|
||||
}
|
||||
.slider-input {
|
||||
width: 54px;
|
||||
}
|
||||
`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import "@polymer/paper-tabs/paper-tabs";
|
||||
import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
|
||||
import type { PaperTabElement } from "@polymer/paper-tabs/paper-tab";
|
||||
import type { PaperTabsElement } from "@polymer/paper-tabs/paper-tabs";
|
||||
import { customElement } from "lit-element";
|
||||
import { Constructor } from "../types";
|
||||
|
||||
const PaperTabs = customElements.get("paper-tabs") as Constructor<
|
||||
PaperTabsElement
|
||||
>;
|
||||
|
||||
let subTemplate: HTMLTemplateElement;
|
||||
|
||||
@customElement("ha-tabs")
|
||||
export class HaTabs extends PaperTabs {
|
||||
private _firstTabWidth = 0;
|
||||
|
||||
private _lastTabWidth = 0;
|
||||
|
||||
private _lastLeftHiddenState = false;
|
||||
|
||||
static get template(): HTMLTemplateElement {
|
||||
if (!subTemplate) {
|
||||
subTemplate = (PaperTabs as any).template.cloneNode(true);
|
||||
|
||||
const superStyle = subTemplate.content.querySelector("style");
|
||||
|
||||
// Add "noink" attribute for scroll buttons to disable animation.
|
||||
subTemplate.content
|
||||
.querySelectorAll("paper-icon-button")
|
||||
.forEach((arrow: PaperIconButtonElement) => {
|
||||
arrow.setAttribute("noink", "");
|
||||
});
|
||||
|
||||
superStyle!.appendChild(
|
||||
document.createTextNode(`
|
||||
:host {
|
||||
padding-top: .5px;
|
||||
}
|
||||
.not-visible {
|
||||
display: none;
|
||||
}
|
||||
paper-icon-button {
|
||||
width: 24px;
|
||||
height: 48px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
`)
|
||||
);
|
||||
}
|
||||
return subTemplate;
|
||||
}
|
||||
|
||||
// Get first and last tab's width for _affectScroll
|
||||
public _tabChanged(tab: PaperTabElement, old: PaperTabElement): void {
|
||||
super._tabChanged(tab, old);
|
||||
const tabs = this.querySelectorAll("paper-tab:not(.hide-tab)");
|
||||
if (tabs.length > 0) {
|
||||
this._firstTabWidth = tabs[0].clientWidth;
|
||||
this._lastTabWidth = tabs[tabs.length - 1].clientWidth;
|
||||
}
|
||||
|
||||
// Scroll active tab into view if needed.
|
||||
const selected = this.querySelector(".iron-selected");
|
||||
if (selected) {
|
||||
selected.scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify _affectScroll so that when the scroll arrows appear
|
||||
* while scrolling and the tab container shrinks we can counteract
|
||||
* the jump in tab position so that the scroll still appears smooth.
|
||||
*/
|
||||
public _affectScroll(dx: number): void {
|
||||
if (this._firstTabWidth === 0 || this._lastTabWidth === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$.tabsContainer.scrollLeft += dx;
|
||||
|
||||
const scrollLeft = this.$.tabsContainer.scrollLeft;
|
||||
|
||||
this._leftHidden = scrollLeft - this._firstTabWidth < 0;
|
||||
this._rightHidden =
|
||||
scrollLeft + this._lastTabWidth > this._tabContainerScrollSize;
|
||||
|
||||
if (this._lastLeftHiddenState !== this._leftHidden) {
|
||||
this._lastLeftHiddenState = this._leftHidden;
|
||||
this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-tabs": HaTabs;
|
||||
}
|
||||
}
|
||||
@@ -378,6 +378,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
: html`
|
||||
<div class="container">
|
||||
${this.hass.localize("ui.components.media-browser.no_items")}
|
||||
<br />
|
||||
${currentItem.media_content_id ===
|
||||
"media-source://media_source/local/."
|
||||
? html`<br />${this.hass.localize(
|
||||
@@ -398,7 +399,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
<br />
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.local_media_files"
|
||||
)}.`
|
||||
)}`
|
||||
: ""}
|
||||
</div>
|
||||
`}
|
||||
@@ -539,17 +540,20 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
mediaContentType?: string
|
||||
): Promise<MediaPlayerItem> {
|
||||
this._loading = true;
|
||||
const itemData =
|
||||
this.entityId !== BROWSER_PLAYER
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
|
||||
this._loading = false;
|
||||
let itemData: any;
|
||||
try {
|
||||
itemData =
|
||||
this.entityId !== BROWSER_PLAYER
|
||||
? await browseMediaPlayer(
|
||||
this.hass,
|
||||
this.entityId,
|
||||
mediaContentId,
|
||||
mediaContentType
|
||||
)
|
||||
: await browseLocalMediaPlayer(this.hass, mediaContentId);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
return itemData;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,14 @@ class HaUserPicker extends LitElement {
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property() public value?: string;
|
||||
@property() public noUserLabel?: string;
|
||||
|
||||
@property() public value = "";
|
||||
|
||||
@property() public users?: User[];
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
private _sortedUsers = memoizeOne((users?: User[]) => {
|
||||
if (!users) {
|
||||
return [];
|
||||
@@ -40,15 +44,19 @@ class HaUserPicker extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<paper-dropdown-menu-light .label=${this.label}>
|
||||
<paper-dropdown-menu-light
|
||||
.label=${this.label}
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<paper-listbox
|
||||
slot="dropdown-content"
|
||||
.selected=${this._value}
|
||||
.selected=${this.value}
|
||||
attr-for-selected="data-user-id"
|
||||
@iron-select=${this._userChanged}
|
||||
>
|
||||
<paper-icon-item data-user-id="">
|
||||
No user
|
||||
${this.noUserLabel ||
|
||||
this.hass?.localize("ui.components.user-picker.no_user")}
|
||||
</paper-icon-item>
|
||||
${this._sortedUsers(this.users).map(
|
||||
(user) => html`
|
||||
@@ -67,10 +75,6 @@ class HaUserPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (this.users === undefined) {
|
||||
@@ -83,7 +87,7 @@ class HaUserPicker extends LitElement {
|
||||
private _userChanged(ev) {
|
||||
const newValue = ev.detail.item.dataset.userId;
|
||||
|
||||
if (newValue !== this._value) {
|
||||
if (newValue !== this.value) {
|
||||
this.value = ev.detail.value;
|
||||
setTimeout(() => {
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
@@ -111,3 +115,9 @@ class HaUserPicker extends LitElement {
|
||||
}
|
||||
|
||||
customElements.define("ha-user-picker", HaUserPicker);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-user-picker": HaUserPicker;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { PolymerChangedEvent } from "../../polymer-types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { fetchUsers, User } from "../../data/user";
|
||||
import "./ha-user-picker";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { guard } from "lit-html/directives/guard";
|
||||
|
||||
@customElement("ha-users-picker")
|
||||
class HaUsersPickerLight extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property() public value?: string[];
|
||||
|
||||
@property({ attribute: "picked-user-label" })
|
||||
public pickedUserLabel?: string;
|
||||
|
||||
@property({ attribute: "pick-user-label" })
|
||||
public pickUserLabel?: string;
|
||||
|
||||
@property({ attribute: false })
|
||||
public users?: User[];
|
||||
|
||||
protected firstUpdated(changedProps) {
|
||||
super.firstUpdated(changedProps);
|
||||
if (this.users === undefined) {
|
||||
fetchUsers(this.hass!).then((users) => {
|
||||
this.users = users;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this.users) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const notSelectedUsers = this._notSelectedUsers(this.users, this.value);
|
||||
return html`
|
||||
${guard([notSelectedUsers], () =>
|
||||
this.value?.map(
|
||||
(user_id, idx) => html`
|
||||
<div>
|
||||
<ha-user-picker
|
||||
.label=${this.pickedUserLabel}
|
||||
.noUserLabel=${this.hass?.localize(
|
||||
"ui.components.user-picker.remove_user"
|
||||
)}
|
||||
.index=${idx}
|
||||
.hass=${this.hass}
|
||||
.value=${user_id}
|
||||
.users=${this._notSelectedUsersAndSelected(
|
||||
user_id,
|
||||
this.users,
|
||||
notSelectedUsers
|
||||
)}
|
||||
@value-changed=${this._userChanged}
|
||||
></ha-user-picker>
|
||||
<mwc-icon-button .userId=${user_id} @click=${this._removeUser}>
|
||||
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
|
||||
</mwc-icon-button>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
)}
|
||||
<ha-user-picker
|
||||
.noUserLabel=${this.pickUserLabel ||
|
||||
this.hass?.localize("ui.components.user-picker.add_user")}
|
||||
.hass=${this.hass}
|
||||
.users=${notSelectedUsers}
|
||||
.disabled=${!notSelectedUsers?.length}
|
||||
@value-changed=${this._addUser}
|
||||
></ha-user-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
private _notSelectedUsers = memoizeOne(
|
||||
(users?: User[], currentUsers?: string[]) =>
|
||||
currentUsers
|
||||
? users?.filter(
|
||||
(user) => !user.system_generated && !currentUsers.includes(user.id)
|
||||
)
|
||||
: users?.filter((user) => !user.system_generated)
|
||||
);
|
||||
|
||||
private _notSelectedUsersAndSelected = (
|
||||
userId: string,
|
||||
users?: User[],
|
||||
notSelected?: User[]
|
||||
) => {
|
||||
const selectedUser = users?.find((user) => user.id === userId);
|
||||
if (selectedUser) {
|
||||
return notSelected ? [...notSelected, selectedUser] : [selectedUser];
|
||||
}
|
||||
return notSelected;
|
||||
};
|
||||
|
||||
private get _currentUsers() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
private async _updateUsers(users) {
|
||||
this.value = users;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: users,
|
||||
});
|
||||
}
|
||||
|
||||
private _userChanged(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const index = (event.currentTarget as any).index;
|
||||
const newValue = event.detail.value;
|
||||
const newUsers = [...this._currentUsers];
|
||||
if (newValue === "") {
|
||||
newUsers.splice(index, 1);
|
||||
} else {
|
||||
newUsers.splice(index, 1, newValue);
|
||||
}
|
||||
this._updateUsers(newUsers);
|
||||
}
|
||||
|
||||
private async _addUser(event: PolymerChangedEvent<string>) {
|
||||
event.stopPropagation();
|
||||
const toAdd = event.detail.value;
|
||||
(event.currentTarget as any).value = "";
|
||||
if (!toAdd) {
|
||||
return;
|
||||
}
|
||||
const currentUsers = this._currentUsers;
|
||||
if (currentUsers.includes(toAdd)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateUsers([...currentUsers, toAdd]);
|
||||
}
|
||||
|
||||
private _removeUser(event) {
|
||||
const userId = (event.currentTarget as any).userId;
|
||||
this._updateUsers(this._currentUsers.filter((user) => user !== userId));
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-users-picker": HaUsersPickerLight;
|
||||
}
|
||||
}
|
||||
@@ -109,10 +109,17 @@ export interface TemplateTrigger {
|
||||
value_template: string;
|
||||
}
|
||||
|
||||
export interface ContextConstraint {
|
||||
context_id?: string;
|
||||
parent_id?: string;
|
||||
user_id?: string | string[];
|
||||
}
|
||||
|
||||
export interface EventTrigger {
|
||||
platform: "event";
|
||||
event_type: string;
|
||||
event_data: any;
|
||||
event_data?: any;
|
||||
context?: ContextConstraint;
|
||||
}
|
||||
|
||||
export type Trigger =
|
||||
|
||||
@@ -11,6 +11,7 @@ export const DISCOVERY_SOURCES = [
|
||||
"ssdp",
|
||||
"zeroconf",
|
||||
"discovery",
|
||||
"mqtt",
|
||||
];
|
||||
|
||||
export const ATTENTION_SOURCES = ["reauth"];
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export interface Counter {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
initial?: number;
|
||||
restore?: boolean;
|
||||
minimum?: number;
|
||||
maximum?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
export interface CounterMutableParams {
|
||||
name: string;
|
||||
icon: string;
|
||||
initial: number;
|
||||
restore: boolean;
|
||||
minimum: number;
|
||||
maximum: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export const fetchCounter = (hass: HomeAssistant) =>
|
||||
hass.callWS<Counter[]>({ type: "counter/list" });
|
||||
|
||||
export const createCounter = (
|
||||
hass: HomeAssistant,
|
||||
values: CounterMutableParams
|
||||
) =>
|
||||
hass.callWS<Counter>({
|
||||
type: "counter/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateCounter = (
|
||||
hass: HomeAssistant,
|
||||
id: string,
|
||||
updates: Partial<CounterMutableParams>
|
||||
) =>
|
||||
hass.callWS<Counter>({
|
||||
type: "counter/update",
|
||||
counter_id: id,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteCounter = (hass: HomeAssistant, id: string) =>
|
||||
hass.callWS({
|
||||
type: "counter/delete",
|
||||
counter_id: id,
|
||||
});
|
||||
+51
-58
@@ -2,78 +2,71 @@ import { HomeAssistant } from "../../types";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||
|
||||
export interface HassioAddonInfo {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
repository: "core" | "local" | string;
|
||||
version: string;
|
||||
state: "none" | "started" | "stopped";
|
||||
installed: string | undefined;
|
||||
detached: boolean;
|
||||
advanced: boolean;
|
||||
available: boolean;
|
||||
build: boolean;
|
||||
advanced: boolean;
|
||||
url: string | null;
|
||||
description: string;
|
||||
detached: boolean;
|
||||
icon: boolean;
|
||||
installed: boolean;
|
||||
logo: boolean;
|
||||
name: string;
|
||||
repository: "core" | "local" | string;
|
||||
slug: string;
|
||||
stage: "stable" | "experimental" | "deprecated";
|
||||
state: "started" | "stopped" | null;
|
||||
update_available: boolean;
|
||||
url: string | null;
|
||||
version_latest: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface HassioAddonDetails extends HassioAddonInfo {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string;
|
||||
long_description: null | string;
|
||||
auto_update: boolean;
|
||||
url: null | string;
|
||||
detached: boolean;
|
||||
documentation: boolean;
|
||||
available: boolean;
|
||||
arch: "armhf" | "aarch64" | "i386" | "amd64";
|
||||
machine: any;
|
||||
homeassistant: string;
|
||||
version_latest: string;
|
||||
boot: "auto" | "manual";
|
||||
build: boolean;
|
||||
options: Record<string, unknown>;
|
||||
network: null | Record<string, number>;
|
||||
network_description: null | Record<string, string>;
|
||||
host_network: boolean;
|
||||
host_pid: boolean;
|
||||
host_ipc: boolean;
|
||||
host_dbus: boolean;
|
||||
privileged: any;
|
||||
apparmor: "disable" | "default" | "profile";
|
||||
devices: string[];
|
||||
auto_uart: boolean;
|
||||
icon: boolean;
|
||||
logo: boolean;
|
||||
stage: "stable" | "experimental" | "deprecated";
|
||||
changelog: boolean;
|
||||
hassio_api: boolean;
|
||||
hassio_role: "default" | "homeassistant" | "manager" | "admin";
|
||||
startup: "initialize" | "system" | "services" | "application" | "once";
|
||||
homeassistant_api: boolean;
|
||||
auth_api: boolean;
|
||||
full_access: boolean;
|
||||
protected: boolean;
|
||||
rating: "1-6";
|
||||
stdin: boolean;
|
||||
webui: null | string;
|
||||
gpio: boolean;
|
||||
kernel_modules: boolean;
|
||||
devicetree: boolean;
|
||||
docker_api: boolean;
|
||||
audio: boolean;
|
||||
arch: "armhf" | "aarch64" | "i386" | "amd64";
|
||||
audio_input: null | string;
|
||||
audio_output: null | string;
|
||||
services_role: string[];
|
||||
audio: boolean;
|
||||
auth_api: boolean;
|
||||
auto_uart: boolean;
|
||||
auto_update: boolean;
|
||||
boot: "auto" | "manual";
|
||||
changelog: boolean;
|
||||
devices: string[];
|
||||
devicetree: boolean;
|
||||
discovery: string[];
|
||||
ip_address: string;
|
||||
ingress: boolean;
|
||||
ingress_panel: boolean;
|
||||
docker_api: boolean;
|
||||
documentation: boolean;
|
||||
full_access: boolean;
|
||||
gpio: boolean;
|
||||
hassio_api: boolean;
|
||||
hassio_role: "default" | "homeassistant" | "manager" | "admin";
|
||||
homeassistant_api: boolean;
|
||||
homeassistant: string;
|
||||
host_dbus: boolean;
|
||||
host_ipc: boolean;
|
||||
host_network: boolean;
|
||||
host_pid: boolean;
|
||||
ingress_entry: null | string;
|
||||
ingress_panel: boolean;
|
||||
ingress_url: null | string;
|
||||
ingress: boolean;
|
||||
ip_address: string;
|
||||
kernel_modules: boolean;
|
||||
long_description: null | string;
|
||||
machine: any;
|
||||
network_description: null | Record<string, string>;
|
||||
network: null | Record<string, number>;
|
||||
options: Record<string, unknown>;
|
||||
privileged: any;
|
||||
protected: boolean;
|
||||
rating: "1-6";
|
||||
services_role: string[];
|
||||
slug: string;
|
||||
startup: "initialize" | "system" | "services" | "application" | "once";
|
||||
stdin: boolean;
|
||||
watchdog: null | boolean;
|
||||
webui: null | string;
|
||||
}
|
||||
|
||||
export interface HassioAddonsInfo {
|
||||
|
||||
@@ -22,8 +22,8 @@ export const hassioApiResultExtractor = <T>(response: HassioResponse<T>) =>
|
||||
export const extractApiErrorMessage = (error: any): string => {
|
||||
return typeof error === "object"
|
||||
? typeof error.body === "object"
|
||||
? error.body.message || "Unknown error, see logs"
|
||||
: error.body || "Unknown error, see logs"
|
||||
? error.body.message || "Unknown error, see supervisor logs"
|
||||
: error.body || error.message || "Unknown error, see supervisor logs"
|
||||
: error;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||
|
||||
interface HassioDockerRegistries {
|
||||
[key: string]: { username: string; password?: string };
|
||||
}
|
||||
|
||||
export const fetchHassioDockerRegistries = async (hass: HomeAssistant) => {
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
|
||||
"GET",
|
||||
"hassio/docker/registries"
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const addHassioDockerRegistry = async (
|
||||
hass: HomeAssistant,
|
||||
data: HassioDockerRegistries
|
||||
) => {
|
||||
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
|
||||
"POST",
|
||||
"hassio/docker/registries",
|
||||
data
|
||||
);
|
||||
};
|
||||
|
||||
export const removeHassioDockerRegistry = async (
|
||||
hass: HomeAssistant,
|
||||
registry: string
|
||||
) => {
|
||||
await hass.callApi<HassioResponse<void>>(
|
||||
"DELETE",
|
||||
`hassio/docker/registries/${registry}`
|
||||
);
|
||||
};
|
||||
+16
-5
@@ -1,14 +1,25 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||
|
||||
export type HassioHostInfo = any;
|
||||
export type HassioHostInfo = {
|
||||
chassis: string;
|
||||
cpe: string;
|
||||
deployment: string;
|
||||
disk_free: number;
|
||||
disk_total: number;
|
||||
disk_used: number;
|
||||
features: string[];
|
||||
hostname: string;
|
||||
kernel: string;
|
||||
operating_system: string;
|
||||
};
|
||||
|
||||
export interface HassioHassOSInfo {
|
||||
version: string;
|
||||
version_cli: string;
|
||||
board: string;
|
||||
boot: string;
|
||||
update_available: boolean;
|
||||
version_latest: string;
|
||||
version_cli_latest: string;
|
||||
board: "ova" | "rpi";
|
||||
version: string;
|
||||
}
|
||||
|
||||
export const fetchHassioHostInfo = async (hass: HomeAssistant) => {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { HassioResponse } from "./common";
|
||||
import { CreateSessionResponse } from "./supervisor";
|
||||
|
||||
export const createHassioSession = async (hass: HomeAssistant) => {
|
||||
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
|
||||
"POST",
|
||||
"hassio/ingress/session"
|
||||
);
|
||||
document.cookie = `ingress_session=${
|
||||
response.data.session
|
||||
};path=/api/hassio_ingress/;SameSite=Strict${
|
||||
location.protocol === "https:" ? ";Secure" : ""
|
||||
}`;
|
||||
return response.data.session;
|
||||
};
|
||||
|
||||
export const validateHassioSession = async (
|
||||
hass: HomeAssistant,
|
||||
session: string
|
||||
) =>
|
||||
await hass.callApi<HassioResponse<null>>(
|
||||
"POST",
|
||||
"hassio/ingress/validate_session",
|
||||
{ session }
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { HomeAssistant } from "../../types";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||
|
||||
export interface HassioResolution {
|
||||
unsupported: string[];
|
||||
}
|
||||
|
||||
export const fetchHassioResolution = async (hass: HomeAssistant) => {
|
||||
return hassioApiResultExtractor(
|
||||
await hass.callApi<HassioResponse<HassioResolution>>(
|
||||
"GET",
|
||||
"hassio/resolution/info"
|
||||
)
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,56 @@
|
||||
import { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { HassioAddonInfo, HassioAddonRepository } from "./addon";
|
||||
import { hassioApiResultExtractor, HassioResponse } from "./common";
|
||||
|
||||
export type HassioHomeAssistantInfo = any;
|
||||
export type HassioSupervisorInfo = any;
|
||||
export type HassioHomeAssistantInfo = {
|
||||
arch: string;
|
||||
audio_input: string | null;
|
||||
audio_output: string | null;
|
||||
boot: boolean;
|
||||
image: string;
|
||||
ip_address: string;
|
||||
machine: string;
|
||||
port: number;
|
||||
ssl: boolean;
|
||||
update_available: boolean;
|
||||
version_latest: string;
|
||||
version: string;
|
||||
wait_boot: number;
|
||||
watchdog: boolean;
|
||||
};
|
||||
|
||||
export type HassioSupervisorInfo = {
|
||||
addons: HassioAddonInfo[];
|
||||
addons_repositories: HassioAddonRepository[];
|
||||
arch: string;
|
||||
channel: string;
|
||||
debug: boolean;
|
||||
debug_block: boolean;
|
||||
diagnostics: boolean | null;
|
||||
healthy: boolean;
|
||||
ip_address: string;
|
||||
logging: string;
|
||||
supported: boolean;
|
||||
timezone: string;
|
||||
update_available: boolean;
|
||||
version: string;
|
||||
version_latest: string;
|
||||
wait_boot: number;
|
||||
};
|
||||
|
||||
export type HassioInfo = {
|
||||
arch: string;
|
||||
channel: string;
|
||||
docker: string;
|
||||
hassos?: string;
|
||||
features: string[];
|
||||
hassos: null;
|
||||
homeassistant: string;
|
||||
hostname: string;
|
||||
logging: string;
|
||||
maching: string;
|
||||
machine: string;
|
||||
operating_system: string;
|
||||
supervisor: string;
|
||||
supported: boolean;
|
||||
supported_arch: string[];
|
||||
timezone: string;
|
||||
};
|
||||
@@ -74,18 +111,6 @@ export const fetchHassioLogs = async (
|
||||
return hass.callApi<string>("GET", `hassio/${provider}/logs`);
|
||||
};
|
||||
|
||||
export const createHassioSession = async (hass: HomeAssistant) => {
|
||||
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
|
||||
"POST",
|
||||
"hassio/ingress/session"
|
||||
);
|
||||
document.cookie = `ingress_session=${
|
||||
response.data.session
|
||||
};path=/api/hassio_ingress/;SameSite=Strict${
|
||||
location.protocol === "https:" ? ";Secure" : ""
|
||||
}`;
|
||||
};
|
||||
|
||||
export const setSupervisorOption = async (
|
||||
hass: HomeAssistant,
|
||||
data: SupervisorOptions
|
||||
|
||||
+5
-2
@@ -85,11 +85,14 @@ export const fetchRecent = (
|
||||
export const fetchDate = (
|
||||
hass: HomeAssistant,
|
||||
startTime: Date,
|
||||
endTime: Date
|
||||
endTime: Date,
|
||||
entityId
|
||||
): Promise<HassEntity[][]> => {
|
||||
return hass.callApi(
|
||||
"GET",
|
||||
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response`
|
||||
`history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
|
||||
entityId ? `&filter_entity_id=${entityId}` : ``
|
||||
}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export interface LovelaceResource {
|
||||
}
|
||||
|
||||
export interface LovelaceResourcesMutableParams {
|
||||
res_type: "css" | "js" | "module" | "html";
|
||||
res_type: LovelaceResource["type"];
|
||||
url: string;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ export interface LovelaceViewConfig {
|
||||
export interface LovelaceViewElement extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: Lovelace;
|
||||
narrow?: boolean;
|
||||
index?: number;
|
||||
cards?: Array<LovelaceCard | HuiErrorCard>;
|
||||
badges?: LovelaceBadge[];
|
||||
|
||||
@@ -63,6 +63,16 @@ export interface OZWNetworkStatistics {
|
||||
retries: number;
|
||||
}
|
||||
|
||||
export interface OZWDeviceConfig {
|
||||
label: string;
|
||||
type: string;
|
||||
value: string | number;
|
||||
parameter: number;
|
||||
min: number;
|
||||
max: number;
|
||||
help: string;
|
||||
}
|
||||
|
||||
export const nodeQueryStages = [
|
||||
"ProtocolInfo",
|
||||
"Probe",
|
||||
@@ -180,6 +190,17 @@ export const fetchOZWNodeMetadata = (
|
||||
node_id: node_id,
|
||||
});
|
||||
|
||||
export const fetchOZWNodeConfig = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
node_id: number
|
||||
): Promise<OZWDeviceConfig[]> =>
|
||||
hass.callWS({
|
||||
type: "ozw/get_config_parameters",
|
||||
ozw_instance: ozw_instance,
|
||||
node_id: node_id,
|
||||
});
|
||||
|
||||
export const refreshNodeInfo = (
|
||||
hass: HomeAssistant,
|
||||
ozw_instance: number,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export const removeTasmotaDeviceEntry = (
|
||||
hass: HomeAssistant,
|
||||
deviceId: string
|
||||
): Promise<void> =>
|
||||
hass.callWS({
|
||||
type: "tasmota/device/remove",
|
||||
device_id: deviceId,
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
export type TimerEntity = HassEntityBase & {
|
||||
attributes: HassEntityAttributeBase & {
|
||||
@@ -9,3 +10,48 @@ export type TimerEntity = HassEntityBase & {
|
||||
remaining: string;
|
||||
};
|
||||
};
|
||||
|
||||
export interface DurationDict {
|
||||
hours?: number | string;
|
||||
minutes?: number | string;
|
||||
seconds?: number | string;
|
||||
}
|
||||
|
||||
export interface Timer {
|
||||
id: string;
|
||||
name: string;
|
||||
icon?: string;
|
||||
duration?: string | number | DurationDict;
|
||||
}
|
||||
|
||||
export interface TimerMutableParams {
|
||||
name: string;
|
||||
icon: string;
|
||||
duration: string | number | DurationDict;
|
||||
}
|
||||
|
||||
export const fetchTimer = (hass: HomeAssistant) =>
|
||||
hass.callWS<Timer[]>({ type: "timer/list" });
|
||||
|
||||
export const createTimer = (hass: HomeAssistant, values: TimerMutableParams) =>
|
||||
hass.callWS<Timer>({
|
||||
type: "timer/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateTimer = (
|
||||
hass: HomeAssistant,
|
||||
id: string,
|
||||
updates: Partial<TimerMutableParams>
|
||||
) =>
|
||||
hass.callWS<Timer>({
|
||||
type: "timer/update",
|
||||
timer_id: id,
|
||||
...updates,
|
||||
});
|
||||
|
||||
export const deleteTimer = (hass: HomeAssistant, id: string) =>
|
||||
hass.callWS({
|
||||
type: "timer/delete",
|
||||
timer_id: id,
|
||||
});
|
||||
|
||||
+38
-19
@@ -1,6 +1,14 @@
|
||||
import { SVGTemplateResult, svg, html, TemplateResult, css } from "lit-element";
|
||||
import {
|
||||
mdiGauge,
|
||||
mdiWaterPercent,
|
||||
mdiWeatherFog,
|
||||
mdiWeatherRainy,
|
||||
mdiWeatherWindy,
|
||||
} from "@mdi/js";
|
||||
import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit-element";
|
||||
import { styleMap } from "lit-html/directives/style-map";
|
||||
|
||||
import "../components/ha-icon";
|
||||
import "../components/ha-svg-icon";
|
||||
import type { HomeAssistant, WeatherEntity } from "../types";
|
||||
import { roundWithOneDecimal } from "../util/calculate";
|
||||
|
||||
@@ -25,6 +33,15 @@ export const weatherIcons = {
|
||||
exceptional: "hass:alert-circle-outline",
|
||||
};
|
||||
|
||||
export const weatherAttrIcons = {
|
||||
humidity: mdiWaterPercent,
|
||||
wind_bearing: mdiWeatherWindy,
|
||||
wind_speed: mdiWeatherWindy,
|
||||
pressure: mdiGauge,
|
||||
visibility: mdiWeatherFog,
|
||||
precipitation: mdiWeatherRainy,
|
||||
};
|
||||
|
||||
const cloudyStates = new Set<string>([
|
||||
"partlycloudy",
|
||||
"cloudy",
|
||||
@@ -110,6 +127,7 @@ export const getWeatherUnit = (
|
||||
return lengthUnit === "km" ? "hPa" : "inHg";
|
||||
case "wind_speed":
|
||||
return `${lengthUnit}/h`;
|
||||
case "visibility":
|
||||
case "length":
|
||||
return lengthUnit;
|
||||
case "precipitation":
|
||||
@@ -125,7 +143,7 @@ export const getWeatherUnit = (
|
||||
export const getSecondaryWeatherAttribute = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity
|
||||
): string | undefined => {
|
||||
): TemplateResult | undefined => {
|
||||
const extrema = getWeatherExtrema(hass, stateObj);
|
||||
|
||||
if (extrema) {
|
||||
@@ -149,17 +167,22 @@ export const getSecondaryWeatherAttribute = (
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `
|
||||
${hass!.localize(
|
||||
`ui.card.weather.attributes.${attribute}`
|
||||
)} ${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
|
||||
const weatherAttrIcon = weatherAttrIcons[attribute];
|
||||
|
||||
return html`
|
||||
${weatherAttrIcon
|
||||
? html`
|
||||
<ha-svg-icon class="attr-icon" .path=${weatherAttrIcon}></ha-svg-icon>
|
||||
`
|
||||
: hass!.localize(`ui.card.weather.attributes.${attribute}`)}
|
||||
${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
|
||||
`;
|
||||
};
|
||||
|
||||
const getWeatherExtrema = (
|
||||
hass: HomeAssistant,
|
||||
stateObj: WeatherEntity
|
||||
): string | undefined => {
|
||||
): TemplateResult | undefined => {
|
||||
if (!stateObj.attributes.forecast?.length) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -189,22 +212,18 @@ const getWeatherExtrema = (
|
||||
|
||||
const unit = getWeatherUnit(hass!, "temperature");
|
||||
|
||||
return `
|
||||
${
|
||||
tempHigh
|
||||
? `
|
||||
return html`
|
||||
${tempHigh
|
||||
? `
|
||||
${tempHigh} ${unit}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
: ""}
|
||||
${tempLow && tempHigh ? " / " : ""}
|
||||
${
|
||||
tempLow
|
||||
? `
|
||||
${tempLow
|
||||
? `
|
||||
${tempLow} ${unit}
|
||||
`
|
||||
: ""
|
||||
}
|
||||
: ""}
|
||||
`;
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ interface TemplateListeners {
|
||||
all: boolean;
|
||||
domains: string[];
|
||||
entities: string[];
|
||||
time: boolean;
|
||||
}
|
||||
|
||||
export const subscribeRenderTemplate = (
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable";
|
||||
import "@polymer/paper-input/paper-input";
|
||||
import "@material/mwc-button/mwc-button";
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
@@ -10,16 +9,16 @@ import {
|
||||
internalProperty,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../components/dialog/ha-paper-dialog";
|
||||
import "../../components/ha-dialog";
|
||||
import "../../components/ha-circular-progress";
|
||||
import "../../components/ha-switch";
|
||||
import "../../components/ha-formfield";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaSwitch } from "../../components/ha-switch";
|
||||
import {
|
||||
getConfigEntrySystemOptions,
|
||||
updateConfigEntrySystemOptions,
|
||||
} from "../../data/config_entries";
|
||||
import type { PolymerChangedEvent } from "../../polymer-types";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { ConfigEntrySystemOptionsDialogParams } from "./show-dialog-config-entry-system-options";
|
||||
@@ -35,9 +34,9 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
|
||||
@internalProperty() private _params?: ConfigEntrySystemOptionsDialogParams;
|
||||
|
||||
@internalProperty() private _loading?: boolean;
|
||||
@internalProperty() private _loading = false;
|
||||
|
||||
@internalProperty() private _submitting?: boolean;
|
||||
@internalProperty() private _submitting = false;
|
||||
|
||||
public async showDialog(
|
||||
params: ConfigEntrySystemOptionsDialogParams
|
||||
@@ -51,7 +50,12 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
);
|
||||
this._loading = false;
|
||||
this._disableNewEntities = systemOptions.disable_new_entities;
|
||||
await this.updateComplete;
|
||||
}
|
||||
|
||||
public closeDialog(): void {
|
||||
this._error = "";
|
||||
this._params = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
@@ -60,21 +64,17 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-paper-dialog
|
||||
with-backdrop
|
||||
opened
|
||||
@opened-changed="${this._openedChanged}"
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.title",
|
||||
"integration",
|
||||
this.hass.localize(`component.${this._params.entry.domain}.title`) ||
|
||||
this._params.entry.domain
|
||||
)}
|
||||
>
|
||||
<h2>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.title",
|
||||
"integration",
|
||||
this.hass.localize(
|
||||
`component.${this._params.entry.domain}.title`
|
||||
) || this._params.entry.domain
|
||||
)}
|
||||
</h2>
|
||||
<paper-dialog-scrollable>
|
||||
<div>
|
||||
${this._loading
|
||||
? html`
|
||||
<div class="init-spinner">
|
||||
@@ -112,22 +112,22 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
</ha-formfield>
|
||||
</div>
|
||||
`}
|
||||
</paper-dialog-scrollable>
|
||||
${!this._loading
|
||||
? html`
|
||||
<div class="paper-dialog-buttons">
|
||||
<mwc-button
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.config_entry_system_options.update"
|
||||
)}
|
||||
</mwc-button>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</ha-paper-dialog>
|
||||
</div>
|
||||
<mwc-button
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass.localize("ui.common.cancel")}
|
||||
</mwc-button>
|
||||
<mwc-button
|
||||
slot="primaryAction"
|
||||
@click="${this._updateEntry}"
|
||||
.disabled=${this._submitting || this._loading}
|
||||
>
|
||||
${this.hass.localize("ui.dialogs.config_entry_system_options.update")}
|
||||
</mwc-button>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -154,19 +154,10 @@ class DialogConfigEntrySystemOptions extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
|
||||
if (!(ev.detail as any).value) {
|
||||
this._params = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-paper-dialog {
|
||||
max-width: 500px;
|
||||
}
|
||||
.init-spinner {
|
||||
padding: 50px 100px;
|
||||
text-align: center;
|
||||
|
||||
@@ -70,6 +70,7 @@ class DialogBox extends LitElement {
|
||||
<p
|
||||
class=${classMap({
|
||||
"no-bottom-padding": Boolean(this._params.prompt),
|
||||
warning: Boolean(this._params.warning),
|
||||
})}
|
||||
>
|
||||
${this._params.text}
|
||||
@@ -180,6 +181,9 @@ class DialogBox extends LitElement {
|
||||
/* Place above other dialogs */
|
||||
--dialog-z-index: 104;
|
||||
}
|
||||
.warning {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ interface BaseDialogParams {
|
||||
confirmText?: string;
|
||||
text?: string | TemplateResult;
|
||||
title?: string;
|
||||
warning?: boolean;
|
||||
}
|
||||
|
||||
export interface AlertDialogParams extends BaseDialogParams {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user